diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 33a6ef02ace11..54479c5d99c38 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,7 +11,7 @@ Fields marked with (*) are required. Please don't remove the template. ### Preconditions (*) 1. 2. diff --git a/README.md b/README.md index ec8bcdb292ea7..5fa6150d2be02 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,16 @@ Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a ## Install Magento -* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). +* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html). + +## Learn More About GraphQL in Magento 2 + +* [GraphQL Developer Guide](https://devdocs.magento.com/guides/v2.3/graphql/index.html)

Contributing to the Magento 2 Code Base

Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions. -To learn about how to make a contribution, click [here][1]. +To learn about how to contribute, click [here][1]. To learn about issues, click [here][2]. To open an issue, click [here][3]. @@ -33,7 +37,7 @@ The members of this team have been recognized for their outstanding commitment t

Top Contributors

-Magento is thankful for any contribution that can improve our code base, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. +Magento is thankful for any contribution that can improve our codebase, documentation or increase test coverage. We always recognize our most active members, as their contributions are the foundation of the Magento Open Source platform. @@ -44,7 +48,7 @@ Please review the [Code Contributions guide](https://devdocs.magento.com/guides/ ## Reporting Security Issues -To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up your issue. +To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up on your issue. Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). @@ -60,7 +64,7 @@ Please see LICENSE_EE.txt for the full text of the MEE License or visit https:// ## Community Engineering Slack -To connect with Magento and the Community, join us on the [Magento Community Engineering Slack](https://magentocommeng.slack.com). If you are interested in joining Slack, or a specific channel, send us request at [engcom@adobe.com](mailto:engcom@adobe.com) or [self signup](https://tinyurl.com/engcom-slack). +To connect with Magento and the Community, join us on the [Magento Community Engineering Slack](https://magentocommeng.slack.com). If you are interested in joining Slack, or a specific channel, send us a request at [engcom@adobe.com](mailto:engcom@adobe.com) or [self signup](https://opensource.magento.com/slack). We have channels for each project. These channels are recommended for new members: diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml index 29d928ced2084..8849687611193 100644 --- a/app/code/Magento/AdminNotification/etc/db_schema.xml +++ b/app/code/Magento/AdminNotification/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + comment="Notification ID"/>
- + isSuitable()) { $this->_productTypeModels[$productTypeName] = $model; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_disabledAttrs = array_merge($this->_disabledAttrs, $model->getDisabledAttrs()); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_indexValueAttributes = array_merge( $this->_indexValueAttributes, $model->getIndexValueAttributes() @@ -197,6 +201,7 @@ protected function initTypeModels() public function export() { //Execution time may be very long + // phpcs:ignore Magento2.Functions.DiscouragedFunction set_time_limit(0); $writer = $this->getWriter(); @@ -211,6 +216,7 @@ public function export() if ($entityCollection->count() == 0) { break; } + $entityCollection->clear(); $exportData = $this->getExportData(); foreach ($exportData as $dataRow) { $writer->writeRow($dataRow); @@ -234,16 +240,6 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit foreach ($collection as $attribute) { if (in_array($attribute->getAttributeCode(), $this->_disabledAttrs)) { - if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP])) { - if ($attribute->getAttributeCode() == ImportAdvancedPricing::COL_TIER_PRICE - && in_array( - $attribute->getId(), - $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP] - ) - ) { - $this->_passTierPrice = 1; - } - } $collection->removeItemByKey($attribute->getId()); } } @@ -363,6 +359,7 @@ private function prepareExportData( $linkedTierPricesData = []; foreach ($tierPricesData as $tierPriceData) { $sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']]; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $linkedTierPricesData[] = array_merge( $tierPriceData, [ImportAdvancedPricing::COL_SKU => $sku] @@ -471,7 +468,7 @@ private function fetchTierPrices(array $productIds): array ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty', ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value', ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE => 'ap.percentage_value', - 'product_link_id' => 'ap.' .$productEntityLinkField, + 'product_link_id' => 'ap.' . $productEntityLinkField, ]; if ($exportFilter && array_key_exists('tier_price', $exportFilter)) { if (!empty($exportFilter['tier_price'][0])) { @@ -488,7 +485,7 @@ private function fetchTierPrices(array $productIds): array $selectFields ) ->where( - 'ap.'.$productEntityLinkField.' IN (?)', + 'ap.' . $productEntityLinkField . ' IN (?)', $productIds ); @@ -602,7 +599,7 @@ protected function _getWebsiteCode(int $websiteId): string } if ($storeName && $currencyCode) { - $code = $storeName.' ['.$currencyCode.']'; + $code = $storeName . ' [' . $currencyCode . ']'; } else { $code = $storeName; } diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml index 2dd8c68e2d5fd..bf85a23782095 100644 --- a/app/code/Magento/AdvancedSearch/etc/db_schema.xml +++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml @@ -9,11 +9,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
- + + default="0" comment="Query ID"/> + default="0" comment="Relation ID"/> diff --git a/app/code/Magento/Analytics/Model/ExportDataHandler.php b/app/code/Magento/Analytics/Model/ExportDataHandler.php index dc17a548763eb..72a8e4ea00347 100644 --- a/app/code/Magento/Analytics/Model/ExportDataHandler.php +++ b/app/code/Magento/Analytics/Model/ExportDataHandler.php @@ -89,7 +89,7 @@ public function __construct( public function prepareExportData() { try { - $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath()); $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath()); @@ -157,7 +157,9 @@ private function prepareDirectory(WriteInterface $directory, $path) private function prepareFileDirectory(WriteInterface $directory, $path) { $directory->delete($path); + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (dirname($path) !== '.') { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $directory->create(dirname($path)); } @@ -176,6 +178,7 @@ private function pack($source, $destination) $this->archive->pack( $source, $destination, + // phpcs:ignore Magento2.Functions.DiscouragedFunction is_dir($source) ?: false ); diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md index aa424182e2ebd..7ec30c6dd484b 100644 --- a/app/code/Magento/Analytics/README.md +++ b/app/code/Magento/Analytics/README.md @@ -1,18 +1,27 @@ -# Magento_Analytics Module +# Magento_Analytics module The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](https://devdocs.magento.com/guides/v2.3/advanced-reporting/modules.html) functionality. The module implements the following functionality: -* enabling subscription to the MBI and automatic re-subscription -* changing the base URL with the same MBI account remained -* declaring the configuration schemas for report data collection -* collecting the Magento instance data as reports for the MBI -* introducing API that provides the collected data -* extending Magento configuration with the module parameters: - * subscription status (enabled/disabled) - * industry (a business area in which the instance website works) - * time of data collection (time of the day when the module collects data) +- Enabling subscription to Magento Business Intelligence (MBI) and automatic re-subscription +- Declaring the configuration schemas for report data collection +- Collecting the Magento instance data as reports for MBI +- Introducing API that provides the collected data +- Extending Magento configuration with the module parameters: + - Subscription status (enabled/disabled) + - Industry (a business area in which the instance website works) + - Time of data collection (time of the day when the module collects data) + +## Installation details + +Before disabling or uninstalling this module, note that the following modules depends on this module: +- Magento_CatalogAnalytics +- Magento_CustomerAnalytics +- Magento_QuoteAnalytics +- Magento_ReviewAnalytics +- Magento_SalesAnalytics +- Magento_WishlistAnalytics ## Structure @@ -29,12 +38,12 @@ The subscription to the MBI service is enabled during the installation process o Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab. The following options can be adjusted: -* Advanced Reporting Service (Enabled/Disabled) - * Alters the status of the Advanced Reporting subscription -* Time of day to send data (Hour/Minute/Second in the store's time zone) - * Defines when the data collection process for the Advanced Reporting service occurs -* Industry - * Defines the industry of the store in order to create a personalized Advanced Reporting experience +- Advanced Reporting Service (Enabled/Disabled) + - Alters the status of the Advanced Reporting subscription +- Time of day to send data (Hour/Minute/Second in the store's time zone) + - Defines when the data collection process for the Advanced Reporting service occurs +- Industry + - Defines the industry of the store in order to create a personalized Advanced Reporting experience ## Extensibility diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index 58e62500b8203..8ebd8cb594bee 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -25,9 +25,9 @@ - - - + + + diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php index cf00556cfe590..493fe71c9fbfc 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php @@ -13,7 +13,7 @@ use Magento\Framework\Archive; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; class ExportDataHandlerTest extends \PHPUnit\Framework\TestCase @@ -137,7 +137,7 @@ public function testPrepareExportData($isArchiveSourceDirectory) $this->filesystemMock ->expects($this->once()) ->method('getDirectoryWrite') - ->with(DirectoryList::SYS_TMP) + ->with(DirectoryList::VAR_DIR) ->willReturn($this->directoryMock); $this->directoryMock ->expects($this->exactly(4)) @@ -238,7 +238,7 @@ public function testPrepareExportDataWithLocalizedException() $this->filesystemMock ->expects($this->once()) ->method('getDirectoryWrite') - ->with(DirectoryList::SYS_TMP) + ->with(DirectoryList::VAR_DIR) ->willReturn($this->directoryMock); $this->reportWriterMock ->expects($this->once()) diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php b/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php new file mode 100644 index 0000000000000..84f0952a836c2 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/OperationSearchResults.php @@ -0,0 +1,18 @@ +Bulk Actions + validate-zero-or-greater validate-digits diff --git a/app/code/Magento/AsynchronousOperations/etc/di.xml b/app/code/Magento/AsynchronousOperations/etc/di.xml index 42b62ff8ea374..94a4c56c19cea 100644 --- a/app/code/Magento/AsynchronousOperations/etc/di.xml +++ b/app/code/Magento/AsynchronousOperations/etc/di.xml @@ -16,7 +16,7 @@ - + diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index fc32fbcaa2e98..042e95806ae18 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -52,6 +52,9 @@ public function __construct( //phpcs:ignore Generic.CodeAnalysis.UselessOverridi /** * @inheritDoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -61,6 +64,9 @@ public function __sleep() /** * @inheritDoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php index 95c67f67852da..a1547a0563461 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php @@ -3,8 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Test\Unit\Model; +use Magento\Authorizenet\Helper\Backend\Data; +use Magento\Authorizenet\Helper\Data as HelperData; +use Magento\Authorizenet\Model\Directpost\Response; +use Magento\Authorizenet\Model\Directpost\Response\Factory as ResponseFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Model\Method\ConfigInterface; use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Simplexml\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -13,118 +24,118 @@ use Magento\Authorizenet\Model\Request; use Magento\Authorizenet\Model\Directpost\Request\Factory; use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; use Magento\Sales\Model\Order\Payment\Transaction\Repository as TransactionRepository; +use PHPUnit\Framework\MockObject_MockBuilder; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use ReflectionClass; /** * Class DirectpostTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class DirectpostTest extends \PHPUnit\Framework\TestCase +class DirectpostTest extends TestCase { const TOTAL_AMOUNT = 100.02; const INVOICE_NUM = '00000001'; const TRANSACTION_ID = '41a23x34fd124'; /** - * @var \Magento\Authorizenet\Model\Directpost + * @var Directpost */ protected $directpost; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|PHPUnit_Framework_MockObject_MockObject */ protected $scopeConfigMock; /** - * @var \Magento\Payment\Model\InfoInterface|\PHPUnit_Framework_MockObject_MockObject + * @var InfoInterface|PHPUnit_Framework_MockObject_MockObject */ protected $paymentMock; /** - * @var \Magento\Authorizenet\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var HelperData|PHPUnit_Framework_MockObject_MockObject */ protected $dataHelperMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Response\Factory|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseFactory|PHPUnit_Framework_MockObject_MockObject */ protected $responseFactoryMock; /** - * @var TransactionRepository|\PHPUnit_Framework_MockObject_MockObject + * @var TransactionRepository|PHPUnit_Framework_MockObject_MockObject */ protected $transactionRepositoryMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Response|\PHPUnit_Framework_MockObject_MockObject + * @var Response|PHPUnit_Framework_MockObject_MockObject */ protected $responseMock; /** - * @var TransactionService|\PHPUnit_Framework_MockObject_MockObject + * @var TransactionService|PHPUnit_Framework_MockObject_MockObject */ protected $transactionServiceMock; /** - * @var \Magento\Framework\HTTP\ZendClient|\PHPUnit_Framework_MockObject_MockObject + * @var ZendClient|PHPUnit_Framework_MockObject_MockObject */ protected $httpClientMock; /** - * @var \Magento\Authorizenet\Model\Directpost\Request\Factory|\PHPUnit_Framework_MockObject_MockObject + * @var Factory|PHPUnit_Framework_MockObject_MockObject */ protected $requestFactory; /** - * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + * @var PaymentFailuresInterface|PHPUnit_Framework_MockObject_MockObject */ private $paymentFailures; + /** + * @var ZendClientFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $httpClientFactoryMock; + /** * @inheritdoc */ protected function setUp() { - $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->getMock(); - $this->paymentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getOrder', 'getId', 'setAdditionalInformation', 'getAdditionalInformation', - 'setIsTransactionDenied', 'setIsTransactionClosed', 'decrypt', 'getCcLast4', - 'getParentTransactionId', 'getPoNumber' - ]) - ->getMock(); - $this->dataHelperMock = $this->getMockBuilder(\Magento\Authorizenet\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - + $this->initPaymentMock(); $this->initResponseFactoryMock(); + $this->initHttpClientMock(); - $this->transactionRepositoryMock = $this->getMockBuilder( - \Magento\Sales\Model\Order\Payment\Transaction\Repository::class - ) + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)->getMock(); + $this->dataHelperMock = $this->getMockBuilder(HelperData::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock = $this->getMockBuilder(TransactionRepository::class) ->disableOriginalConstructor() ->setMethods(['getByTransactionId']) ->getMock(); - - $this->transactionServiceMock = $this->getMockBuilder(\Magento\Authorizenet\Model\TransactionService::class) + $this->transactionServiceMock = $this->getMockBuilder(TransactionService::class) ->disableOriginalConstructor() ->setMethods(['getTransactionDetails']) ->getMock(); - - $this->paymentFailures = $this->getMockBuilder( - PaymentFailuresInterface::class - ) + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) ->disableOriginalConstructor() ->getMock(); - - $this->requestFactory = $this->getRequestFactoryMock(); - $httpClientFactoryMock = $this->getHttpClientFactoryMock(); + $this->requestFactory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->httpClientFactoryMock = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); $helper = new ObjectManagerHelper($this); $this->directpost = $helper->getObject( - \Magento\Authorizenet\Model\Directpost::class, + Directpost::class, [ 'scopeConfig' => $this->scopeConfigMock, 'dataHelper' => $this->dataHelperMock, @@ -132,18 +143,97 @@ protected function setUp() 'responseFactory' => $this->responseFactoryMock, 'transactionRepository' => $this->transactionRepositoryMock, 'transactionService' => $this->transactionServiceMock, - 'httpClientFactory' => $httpClientFactoryMock, + 'httpClientFactory' => $this->httpClientFactoryMock, 'paymentFailures' => $this->paymentFailures, ] ); } + /** + * Create mock for response factory + * + * @return void + */ + private function initResponseFactoryMock() + { + $this->responseFactoryMock = $this->getMockBuilder(ResponseFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(Response::class) + ->setMethods( + [ + 'isValidHash', + 'getXTransId', + 'getXResponseCode', + 'getXResponseReasonCode', + 'getXResponseReasonText', + 'getXAmount', + 'setXResponseCode', + 'setXResponseReasonCode', + 'setXAvsCode', + 'setXResponseReasonText', + 'setXTransId', + 'setXInvoiceNum', + 'setXAmount', + 'setXMethod', + 'setXType', + 'setData', + 'getData', + 'setXAccountNumber', + '__wakeup' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->responseFactoryMock->expects($this->any())->method('create')->willReturn($this->responseMock); + } + + /** + * Create mock for payment + * + * @return void + */ + private function initPaymentMock() + { + $this->paymentMock = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getOrder', + 'setAmount', + 'setAnetTransType', + 'setXTransId', + 'getId', + 'setAdditionalInformation', + 'getAdditionalInformation', + 'setIsTransactionDenied', + 'setIsTransactionClosed', + 'decrypt', + 'getCcLast4', + 'getParentTransactionId', + 'getPoNumber' + ] + ) + ->getMock(); + } + + /** + * Create a mock for http client + * + * @return void + */ + private function initHttpClientMock() + { + $this->httpClientMock = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->setMethods(['request', 'getBody', '__wakeup']) + ->getMock(); + } + public function testGetConfigInterface() { - $this->assertInstanceOf( - \Magento\Payment\Model\Method\ConfigInterface::class, - $this->directpost->getConfigInterface() - ); + $this->assertInstanceOf(ConfigInterface::class, $this->directpost->getConfigInterface()); } public function testGetConfigValue() @@ -162,7 +252,7 @@ public function testSetDataHelper() $storeId = 'store-id'; $expectedResult = 'relay-url'; - $helperDataMock = $this->getMockBuilder(\Magento\Authorizenet\Helper\Backend\Data::class) + $helperDataMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); @@ -179,7 +269,7 @@ public function testAuthorize() { $paymentAction = 'some_action'; - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/payment_action', 'store', null) ->willReturn($paymentAction); @@ -190,11 +280,143 @@ public function testAuthorize() $this->directpost->authorize($this->paymentMock, 10); } + /** + * @dataProvider dataProviderCaptureWithInvalidAmount + * @expectedExceptionMessage Invalid amount for capture. + * @expectedException \Magento\Framework\Exception\LocalizedException + * + * @param int $invalidAmount + */ + public function testCaptureWithInvalidAmount($invalidAmount) + { + $this->directpost->capture($this->paymentMock, $invalidAmount); + } + + /** + * @return array + */ + public function dataProviderCaptureWithInvalidAmount() + { + return [ + [0], + [0.000], + [-1.000], + [-1], + [null], + ]; + } + + /** + * Test capture has parent transaction id. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCaptureHasParentTransactionId() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->exactly(2))->method('getParentTransactionId')->willReturn(1); + $this->paymentMock->expects($this->once())->method('setAnetTransType')->willReturn('PRIOR_AUTH_CAPTURE'); + + $this->paymentMock->expects($this->once())->method('getId')->willReturn(1); + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getId')->willReturn(1); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $transactionMock = $this->getMockBuilder(Transaction::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock->expects($this->once()) + ->method('getByTransactionId') + ->with(1, 1, 1) + ->willReturn($transactionMock); + + $this->paymentMock->expects($this->once())->method('setXTransId'); + $this->responseMock->expects($this->once())->method('getData')->willReturn([1]); + + $this->directpost->capture($this->paymentMock, 10); + } + + /** + * @@expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCaptureWithoutParentTransactionId() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->once())->method('getParentTransactionId')->willReturn(null); + $this->responseMock->expects($this->once())->method('getData')->willReturn([1]); + + $this->directpost->capture($this->paymentMock, 10); + } + + public function testCaptureWithoutParentTransactionIdWithoutData() + { + $amount = 10; + + $this->paymentMock->expects($this->once())->method('setAmount')->with($amount); + $this->paymentMock->expects($this->exactly(2))->method('getParentTransactionId')->willReturn(null); + $this->responseMock->expects($this->once())->method('getData')->willReturn([]); + + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(0) + ->willReturnSelf(); + + $this->httpClientFactoryMock->expects($this->once())->method('create')->willReturn($this->httpClientMock); + $this->httpClientMock->expects($this->once())->method('request')->willReturnSelf(); + + $this->buildRequestTest(); + $this->postRequestTest(); + + $this->directpost->capture($this->paymentMock, 10); + } + + private function buildRequestTest() + { + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getStoreId')->willReturn(1); + $orderMock->expects($this->exactly(2))->method('getIncrementId')->willReturn(self::INVOICE_NUM); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $this->addRequestMockToRequestFactoryMock(); + } + + private function postRequestTest() + { + $this->httpClientFactoryMock->expects($this->once())->method('create')->willReturn($this->httpClientMock); + $this->httpClientMock->expects($this->once())->method('request')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseReasonCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXResponseReasonText')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXAvsCode')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXTransId')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXInvoiceNum')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXAmount')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXMethod')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setXType')->willReturnSelf(); + $this->responseMock->expects($this->once())->method('setData')->willReturnSelf(); + + $response = $this->getRefundResponseBody( + Directpost::RESPONSE_CODE_APPROVED, + Directpost::RESPONSE_REASON_CODE_APPROVED, + 'Successful' + ); + $this->httpClientMock->expects($this->once())->method('getBody')->willReturn($response); + $this->responseMock->expects($this->once()) + ->method('getXResponseCode') + ->willReturn(Directpost::RESPONSE_CODE_APPROVED); + $this->responseMock->expects($this->once()) + ->method('getXResponseReasonCode') + ->willReturn(Directpost::RESPONSE_REASON_CODE_APPROVED); + $this->dataHelperMock->expects($this->never())->method('wrapGatewayError'); + } + public function testGetCgiUrl() { $url = 'cgi/url'; - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/cgi_url', 'store', null) ->willReturn($url); @@ -204,7 +426,7 @@ public function testGetCgiUrl() public function testGetCgiUrlWithEmptyConfigValue() { - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->once()) ->method('getValue') ->with('payment/authorizenet_directpost/cgi_url', 'store', null) ->willReturn(null); @@ -218,7 +440,7 @@ public function testGetRelayUrl() $url = 'relay/url'; $this->directpost->setData('store', $storeId); - $this->dataHelperMock->expects($this->any()) + $this->dataHelperMock->expects($this->exactly(2)) ->method('getRelayUrl') ->with($storeId) ->willReturn($url); @@ -268,7 +490,7 @@ public function testValidateResponseFailure() */ protected function prepareTestValidateResponse($transMd5, $login, $isValidHash) { - $this->scopeConfigMock->expects($this->any()) + $this->scopeConfigMock->expects($this->exactly(2)) ->method('getValue') ->willReturnMap( [ @@ -276,7 +498,7 @@ protected function prepareTestValidateResponse($transMd5, $login, $isValidHash) ['payment/authorizenet_directpost/login', 'store', null, $login] ] ); - $this->responseMock->expects($this->any()) + $this->responseMock->expects($this->exactly(1)) ->method('isValidHash') ->with($transMd5, $login) ->willReturn($isValidHash); @@ -328,6 +550,20 @@ public function checkResponseCodeSuccessDataProvider() ]; } + /** + * Checks response failures behaviour. + * + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testCheckResponseCodeFailureDefault() + { + $responseCode = 999999; + $this->responseMock->expects($this->once())->method('getXResponseCode')->willReturn($responseCode); + + $this->directpost->checkResponseCode(); + } + /** * Checks response failures behaviour. * @@ -338,34 +574,24 @@ public function checkResponseCodeSuccessDataProvider() * @expectedException \Magento\Framework\Exception\LocalizedException * @dataProvider checkResponseCodeFailureDataProvider */ - public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls): void + public function testCheckResponseCodeFailureDeclinedOrError(int $responseCode, int $failuresHandlerCalls): void { $reasonText = 'reason text'; $this->responseMock->expects($this->once()) ->method('getXResponseCode') ->willReturn($responseCode); - $this->responseMock->expects($this->any()) - ->method('getXResponseReasonText') - ->willReturn($reasonText); - $this->dataHelperMock->expects($this->any()) + $this->responseMock->expects($this->once())->method('getXResponseReasonText')->willReturn($reasonText); + $this->dataHelperMock->expects($this->once()) ->method('wrapGatewayError') ->with($reasonText) ->willReturn(__('Gateway error: %1', $reasonText)); - $orderMock = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderMock->expects($this->exactly($failuresHandlerCalls)) - ->method('getQuoteId') - ->willReturn(1); - - $this->paymentFailures->expects($this->exactly($failuresHandlerCalls)) - ->method('handle') - ->with(1); + $this->paymentFailures->expects($this->exactly($failuresHandlerCalls))->method('handle')->with(1); + $orderMock = $this->getOrderMock($failuresHandlerCalls); - $reflection = new \ReflectionClass($this->directpost); + $orderMock->expects($this->exactly($failuresHandlerCalls))->method('getQuoteId')->willReturn(1); + $reflection = new ReflectionClass($this->directpost); $order = $reflection->getProperty('order'); $order->setAccessible(true); $order->setValue($this->directpost, $orderMock); @@ -381,7 +607,6 @@ public function checkResponseCodeFailureDataProvider(): array return [ ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1], ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1], - ['responseCode' => 999999, 0], ]; } @@ -417,7 +642,7 @@ public function testCanCapture($isGatewayActionsLocked, $canCapture) { $this->directpost->setData('info_instance', $this->paymentMock); - $this->paymentMock->expects($this->any()) + $this->paymentMock->expects($this->once()) ->method('getAdditionalInformation') ->with(Directpost::GATEWAY_ACTIONS_LOCKED_STATE_KEY) ->willReturn($isGatewayActionsLocked); @@ -452,30 +677,16 @@ public function testFetchVoidedTransactionInfo($transactionId, $resultStatus, $r $paymentId = 36; $orderId = 36; - $this->paymentMock->expects(static::once()) - ->method('getId') - ->willReturn($paymentId); - - $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->setMethods(['getId', '__wakeup']) - ->getMock(); - $orderMock->expects(static::once()) - ->method('getId') - ->willReturn($orderId); + $this->paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); - $this->paymentMock->expects(static::once()) - ->method('getOrder') - ->willReturn($orderMock); - - $transactionMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment\Transaction::class) - ->disableOriginalConstructor() - ->getMock(); - $this->transactionRepositoryMock->expects(static::once()) + $orderMock = $this->getOrderMock(); + $orderMock->expects($this->once())->method('getId')->willReturn($orderId); + $this->paymentMock->expects($this->once())->method('getOrder')->willReturn($orderMock); + $transactionMock = $this->getMockBuilder(Transaction::class)->disableOriginalConstructor()->getMock(); + $this->transactionRepositoryMock->expects($this->once()) ->method('getByTransactionId') ->with($transactionId, $paymentId, $orderId) ->willReturn($transactionMock); - $document = $this->getTransactionXmlDocument( $transactionId, TransactionService::PAYMENT_UPDATE_STATUS_CODE_SUCCESS, @@ -483,20 +694,15 @@ public function testFetchVoidedTransactionInfo($transactionId, $resultStatus, $r $responseStatus, $responseCode ); - $this->transactionServiceMock->expects(static::once()) + $this->transactionServiceMock->expects($this->once()) ->method('getTransactionDetails') ->with($this->directpost, $transactionId) ->willReturn($document); // transaction should be closed - $this->paymentMock->expects(static::once()) - ->method('setIsTransactionDenied') - ->with(true); - $this->paymentMock->expects(static::once()) - ->method('setIsTransactionClosed') - ->with(true); - $transactionMock->expects(static::once()) - ->method('close'); + $this->paymentMock->expects($this->once())->method('setIsTransactionDenied')->with(true); + $this->paymentMock->expects($this->once())->method('setIsTransactionClosed')->with(true); + $transactionMock->expects($this->once())->method('close'); $this->directpost->fetchTransactionInfo($this->paymentMock, $transactionId); } @@ -509,60 +715,41 @@ public function testSuccessRefund() { $card = 1111; - $this->paymentMock->expects(static::exactly(2)) - ->method('getCcLast4') - ->willReturn($card); - $this->paymentMock->expects(static::once()) - ->method('decrypt') - ->willReturn($card); - $this->paymentMock->expects(static::exactly(3)) + $this->paymentMock->expects($this->exactly(1))->method('getCcLast4')->willReturn($card); + $this->paymentMock->expects($this->once())->method('decrypt')->willReturn($card); + $this->paymentMock->expects($this->exactly(3)) ->method('getParentTransactionId') ->willReturn(self::TRANSACTION_ID . '-capture'); - $this->paymentMock->expects(static::once()) - ->method('getPoNumber') - ->willReturn(self::INVOICE_NUM); - $this->paymentMock->expects(static::once()) + $this->paymentMock->expects($this->once())->method('getPoNumber')->willReturn(self::INVOICE_NUM); + $this->paymentMock->expects($this->once()) ->method('setIsTransactionClosed') ->with(true) ->willReturnSelf(); + $this->addRequestMockToRequestFactoryMock(); + $orderMock = $this->getOrderMock(); - $this->paymentMock->expects(static::exactly(2)) - ->method('getOrder') - ->willReturn($orderMock); + $orderMock->expects($this->once())->method('getId')->willReturn(1); + $orderMock->expects($this->exactly(2))->method('getIncrementId')->willReturn(self::INVOICE_NUM); + $orderMock->expects($this->once())->method('getStoreId')->willReturn(1); + + $this->paymentMock->expects($this->exactly(2))->method('getOrder')->willReturn($orderMock); - $transactionMock = $this->getMockBuilder(Order\Payment\Transaction::class) + $transactionMock = $this->getMockBuilder(Transaction::class) ->disableOriginalConstructor() ->setMethods(['getAdditionalInformation']) ->getMock(); - $transactionMock->expects(static::once()) + $transactionMock->expects($this->once()) ->method('getAdditionalInformation') ->with(Directpost::REAL_TRANSACTION_ID_KEY) ->willReturn(self::TRANSACTION_ID); - $this->transactionRepositoryMock->expects(static::once()) + $this->transactionRepositoryMock->expects($this->once()) ->method('getByTransactionId') ->willReturn($transactionMock); - $response = $this->getRefundResponseBody( - Directpost::RESPONSE_CODE_APPROVED, - Directpost::RESPONSE_REASON_CODE_APPROVED, - 'Successful' - ); - $this->httpClientMock->expects(static::once()) - ->method('getBody') - ->willReturn($response); - - $this->responseMock->expects(static::once()) - ->method('getXResponseCode') - ->willReturn(Directpost::RESPONSE_CODE_APPROVED); - $this->responseMock->expects(static::once()) - ->method('getXResponseReasonCode') - ->willReturn(Directpost::RESPONSE_REASON_CODE_APPROVED); - - $this->dataHelperMock->expects(static::never()) - ->method('wrapGatewayError'); + $this->postRequestTest(); $this->directpost->refund($this->paymentMock, self::TOTAL_AMOUNT); } @@ -583,65 +770,6 @@ public function dataProviderTransaction() ]; } - /** - * Create mock for response factory - * @return void - */ - private function initResponseFactoryMock() - { - $this->responseFactoryMock = $this->getMockBuilder( - \Magento\Authorizenet\Model\Directpost\Response\Factory::class - )->disableOriginalConstructor()->getMock(); - $this->responseMock = $this->getMockBuilder(\Magento\Authorizenet\Model\Directpost\Response::class) - ->setMethods( - [ - 'isValidHash', - 'getXTransId', 'getXResponseCode', 'getXResponseReasonCode', 'getXResponseReasonText', 'getXAmount', - 'setXResponseCode', 'setXResponseReasonCode', 'setXAvsCode', 'setXResponseReasonText', - 'setXTransId', 'setXInvoiceNum', 'setXAmount', 'setXMethod', 'setXType', 'setData', - 'setXAccountNumber', - '__wakeup' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - - $this->responseMock->expects(static::any()) - ->method('setXResponseCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXResponseReasonCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXResponseReasonText') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXAvsCode') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXTransId') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXInvoiceNum') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXAmount') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXMethod') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setXType') - ->willReturnSelf(); - $this->responseMock->expects(static::any()) - ->method('setData') - ->willReturnSelf(); - - $this->responseFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->responseMock); - } - /** * Get transaction data * @param $transactionId @@ -694,80 +822,40 @@ private function getTransactionXmlDocument( /** * Get mock for authorize.net request factory - * @return \PHPUnit\Framework\MockObject_MockBuilder */ - private function getRequestFactoryMock() + private function addRequestMockToRequestFactoryMock() { - $requestFactory = $this->getMockBuilder(Factory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); $request = $this->getMockBuilder(Request::class) ->disableOriginalConstructor() ->setMethods(['__wakeup']) ->getMock(); - $requestFactory->expects(static::any()) + $this->requestFactory->expects($this->once()) ->method('create') ->willReturn($request); - return $requestFactory; } /** * Get mock for order - * @return \PHPUnit_Framework_MockObject_MockObject + * @return PHPUnit_Framework_MockObject_MockObject */ private function getOrderMock() { - $orderMock = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getId', 'getIncrementId', 'getStoreId', 'getBillingAddress', 'getShippingAddress', - 'getBaseCurrencyCode', 'getBaseTaxAmount', '__wakeup' - ]) - ->getMock(); - - $orderMock->expects(static::once()) - ->method('getId') - ->willReturn(1); - - $orderMock->expects(static::exactly(2)) - ->method('getIncrementId') - ->willReturn(self::INVOICE_NUM); - - $orderMock->expects(static::once()) - ->method('getStoreId') - ->willReturn(1); - - $orderMock->expects(static::once()) - ->method('getBaseCurrencyCode') - ->willReturn('USD'); - return $orderMock; - } - - /** - * Create and return mock for http client factory - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getHttpClientFactoryMock() - { - $this->httpClientMock = $this->getMockBuilder(\Magento\Framework\HTTP\ZendClient::class) + return $this->getMockBuilder(Order::class) ->disableOriginalConstructor() - ->setMethods(['request', 'getBody', '__wakeup']) - ->getMock(); - - $this->httpClientMock->expects(static::any()) - ->method('request') - ->willReturnSelf(); - - $httpClientFactoryMock = $this->getMockBuilder(\Magento\Framework\HTTP\ZendClientFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods( + [ + 'getId', + 'getQuoteId', + 'getIncrementId', + 'getStoreId', + 'getBillingAddress', + 'getShippingAddress', + 'getBaseCurrencyCode', + 'getBaseTaxAmount', + '__wakeup' + ] + ) ->getMock(); - - $httpClientFactoryMock->expects(static::any()) - ->method('create') - ->willReturn($this->httpClientMock); - return $httpClientFactoryMock; } /** @@ -788,7 +876,9 @@ private function getRefundResponseBody($code, $reasonCode, $reasonText) $result[9] = self::TOTAL_AMOUNT; // XAmount $result[10] = Directpost::REQUEST_METHOD_CC; // XMethod $result[11] = Directpost::REQUEST_TYPE_CREDIT; // XType + // @codingStandardsIgnoreStart $result[37] = md5(self::TRANSACTION_ID); // x_MD5_Hash + // @codingStandardsIgnoreEnd $result[50] = '48329483921'; // setXAccountNumber return implode(Directpost::RESPONSE_DELIM_CHAR, $result); } diff --git a/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js b/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js index e43341ca2b337..eb162034bc04d 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js +++ b/app/code/Magento/Authorizenet/view/adminhtml/web/js/direct-post.js @@ -3,18 +3,11 @@ * See COPYING.txt for license details. */ -(function (factory) { - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'mage/backend/validation', - 'prototype' - ], factory); - } else { - factory(jQuery); - } -}(function (jQuery) { - +define([ + 'jquery', + 'mage/backend/validation', + 'prototype' +], function (jQuery) { window.directPost = Class.create(); directPost.prototype = { initialize: function (methodCode, iframeId, controller, orderSaveUrl, cgiUrl, nativeAction) { @@ -349,4 +342,4 @@ } } }; -})); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php index da2b953d843b1..646ad4f195b9d 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php @@ -81,6 +81,9 @@ public function testGetSolutionIdSandbox($environment, $expectedSolution) $this->assertEquals($expectedSolution, $this->model->getSolutionId(123)); } + /** + * @return array + */ public function configMapProvider() { return [ @@ -97,6 +100,10 @@ public function configMapProvider() ['getTransactionInfoSyncKeys', 'transactionSyncKeys', 'a,b,c', ['a', 'b', 'c']], ]; } + + /** + * @return array + */ public function environmentUrlProvider() { return [ @@ -105,6 +112,9 @@ public function environmentUrlProvider() ]; } + /** + * @return array + */ public function environmentSolutionProvider() { return [ diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php index 6ddb30a64af96..84c2f19040e16 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php @@ -108,6 +108,10 @@ public function testBuildWithBothAddresses() $this->assertEquals('abc', $result['transactionRequest']['customerIP']); } + /** + * @param $responseData + * @param $addressPrefix + */ private function validateAddressData($responseData, $addressPrefix) { foreach ($this->mockAddressData as $fieldValue => $field) { @@ -115,6 +119,11 @@ private function validateAddressData($responseData, $addressPrefix) } } + /** + * @param $prefix + * + * @return \PHPUnit\Framework\MockObject\MockObject + */ private function createAddressMock($prefix) { $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php index a52a1b317fbb7..197dc209ece66 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php @@ -112,6 +112,9 @@ public function testDoesNothingWhenPending(string $status) $this->handler->handle($subject, $response); } + /** + * @return array + */ public function pendingTransactionStatusesProvider() { return [ @@ -120,6 +123,9 @@ public function pendingTransactionStatusesProvider() ]; } + /** + * @return array + */ public function declinedTransactionStatusesProvider() { return [ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 891b2a3ada724..284cb01148f68 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -3,26 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Backend\Block\Widget\Grid\Massaction; +use Magento\Backend\Block\Template\Context; +use Magento\Backend\Block\Widget; +use Magento\Backend\Block\Widget\Grid\Column; +use Magento\Backend\Block\Widget\Grid\ColumnSet; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\Json\EncoderInterface; +use Magento\Quote\Model\Quote; /** * Grid widget massaction block * + * phpcs:disable Magento2.Classes.AbstractApi * @api - * @method \Magento\Quote\Model\Quote setHideFormElement(boolean $value) Hide Form element to prevent IE errors + * @method Quote setHideFormElement(boolean $value) Hide Form element to prevent IE errors * @method boolean getHideFormElement() * @deprecated 100.2.0 in favour of UI component implementation * @since 100.0.2 */ -abstract class AbstractMassaction extends \Magento\Backend\Block\Widget +abstract class AbstractMassaction extends Widget { /** - * @var \Magento\Framework\Json\EncoderInterface + * @var EncoderInterface */ protected $_jsonEncoder; @@ -39,13 +48,13 @@ abstract class AbstractMassaction extends \Magento\Backend\Block\Widget protected $_template = 'Magento_Backend::widget/grid/massaction.phtml'; /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param Context $context + * @param EncoderInterface $jsonEncoder * @param array $data */ public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Framework\Json\EncoderInterface $jsonEncoder, + Context $context, + EncoderInterface $jsonEncoder, array $data = [] ) { $this->_jsonEncoder = $jsonEncoder; @@ -122,11 +131,7 @@ private function isVisible(DataObject $item) */ public function getItem($itemId) { - if (isset($this->_items[$itemId])) { - return $this->_items[$itemId]; - } - - return null; + return $this->_items[$itemId] ?? null; } /** @@ -161,7 +166,7 @@ public function getItemsJson() */ public function getCount() { - return sizeof($this->_items); + return count($this->_items); } /** @@ -288,11 +293,11 @@ public function getGridIdsJson() if ($collection instanceof AbstractDb) { $idsSelect = clone $collection->getSelect(); - $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); - $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); - $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); - $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); - $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idsSelect->reset(Select::ORDER); + $idsSelect->reset(Select::LIMIT_COUNT); + $idsSelect->reset(Select::LIMIT_OFFSET); + $idsSelect->reset(Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField()); $idList = $collection->getConnection()->fetchCol($idsSelect); } else { $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); @@ -358,7 +363,7 @@ public function prepareMassactionColumn() { $columnId = 'massaction'; $massactionColumn = $this->getLayout()->createBlock( - \Magento\Backend\Block\Widget\Grid\Column::class + Column::class )->setData( [ 'index' => $this->getMassactionIdField(), @@ -378,7 +383,7 @@ public function prepareMassactionColumn() $gridBlock = $this->getParentBlock(); $massactionColumn->setSelected($this->getSelected())->setGrid($gridBlock)->setId($columnId); - /** @var $columnSetBlock \Magento\Backend\Block\Widget\Grid\ColumnSet */ + /** @var $columnSetBlock ColumnSet */ $columnSetBlock = $gridBlock->getColumnSet(); $childNames = $columnSetBlock->getChildNames(); $siblingElement = count($childNames) ? current($childNames) : 0; diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index 809b78b7b98bc..6d2f8f6a21d4a 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -5,17 +5,20 @@ */ namespace Magento\Backend\Model\Auth; +use Magento\Framework\Acl; +use Magento\Framework\AclFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Backend\Spi\SessionUserHydratorInterface; +use Magento\Backend\Spi\SessionAclHydratorInterface; +use Magento\User\Model\User; +use Magento\User\Model\UserFactory; /** * Backend Auth session model * * @api - * @method \Magento\User\Model\User|null getUser() - * @method \Magento\Backend\Model\Auth\Session setUser(\Magento\User\Model\User $value) - * @method \Magento\Framework\Acl|null getAcl() - * @method \Magento\Backend\Model\Auth\Session setAcl(\Magento\Framework\Acl $value) * @method int getUpdatedAt() * @method \Magento\Backend\Model\Auth\Session setUpdatedAt(int $value) * @@ -56,6 +59,36 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage */ protected $_config; + /** + * @var SessionUserHydratorInterface + */ + private $userHydrator; + + /** + * @var SessionAclHydratorInterface + */ + private $aclHydrator; + + /** + * @var UserFactory + */ + private $userFactory; + + /** + * @var AclFactory + */ + private $aclFactory; + + /** + * @var User|null + */ + private $user; + + /** + * @var Acl|null + */ + private $acl; + /** * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver @@ -70,6 +103,10 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage * @param \Magento\Backend\Model\UrlInterface $backendUrl * @param \Magento\Backend\App\ConfigInterface $config * @throws \Magento\Framework\Exception\SessionException + * @param SessionUserHydratorInterface|null $userHydrator + * @param SessionAclHydratorInterface|null $aclHydrator + * @param UserFactory|null $userFactory + * @param AclFactory|null $aclFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,11 +121,19 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\Acl\Builder $aclBuilder, \Magento\Backend\Model\UrlInterface $backendUrl, - \Magento\Backend\App\ConfigInterface $config + \Magento\Backend\App\ConfigInterface $config, + ?SessionUserHydratorInterface $userHydrator = null, + ?SessionAclHydratorInterface $aclHydrator = null, + ?UserFactory $userFactory = null, + ?AclFactory $aclFactory = null ) { $this->_config = $config; $this->_aclBuilder = $aclBuilder; $this->_backendUrl = $backendUrl; + $this->userHydrator = $userHydrator ?? ObjectManager::getInstance()->get(SessionUserHydratorInterface::class); + $this->aclHydrator = $aclHydrator ?? ObjectManager::getInstance()->get(SessionAclHydratorInterface::class); + $this->userFactory = $userFactory ?? ObjectManager::getInstance()->get(UserFactory::class); + $this->aclFactory = $aclFactory ?? ObjectManager::getInstance()->get(AclFactory::class); parent::__construct( $request, $sidResolver, @@ -232,6 +277,16 @@ public function processLogin() return $this; } + /** + * @inheritDoc + */ + public function destroy(array $options = null) + { + $this->user = null; + $this->acl = null; + parent::destroy($options); + } + /** * Process of configuring of current auth storage when logout was performed * @@ -255,4 +310,142 @@ public function isValidForPath($path) { return true; } + + /** + * Logged-in user. + * + * @return User|null + */ + public function getUser() + { + if (!$this->user) { + $userData = $this->getUserData(); + if ($userData) { + /** @var User $user */ + $user = $this->userFactory->create(); + $this->userHydrator->hydrate($user, $userData); + $this->user = $user; + } elseif ($user = parent::getUser()) { + $this->setUser($user); + } + } + + return $this->user; + } + + /** + * Set logged-in user instance. + * + * @param User|null $user + * @return Session + */ + public function setUser($user) + { + $this->setUserData(null); + if ($user) { + $this->setUserData($this->userHydrator->extract($user)); + } + $this->user = $user; + + return $this; + } + + /** + * Is user logged in? + * + * @return bool + */ + public function hasUser() + { + return (bool)$this->getUser(); + } + + /** + * Remove logged-in user. + * + * @return Session + */ + public function unsUser() + { + $this->user = null; + parent::unsUser(); + return $this->unsUserData(); + } + + /** + * Logged-in user's ACL data. + * + * @return Acl|null + */ + public function getAcl() + { + if (!$this->acl) { + $aclData = $this->getUserAclData(); + if ($aclData) { + /** @var Acl $acl */ + $acl = $this->aclFactory->create(); + $this->aclHydrator->hydrate($acl, $aclData); + $this->acl = $acl; + } elseif ($acl = parent::getAcl()) { + $this->setAcl($acl); + } + } + + return $this->acl; + } + + /** + * Set logged-in user's ACL data instance. + * + * @param Acl|null $acl + * @return Session + */ + public function setAcl($acl) + { + $this->setUserAclData(null); + if ($acl) { + $this->setUserAclData($this->aclHydrator->extract($acl)); + } + $this->acl = $acl; + + return $this; + } + + /** + * Whether ACL data is present. + * + * @return bool + */ + public function hasAcl() + { + return (bool)$this->getAcl(); + } + + /** + * Remove ACL data. + * + * @return Session + */ + public function unsAcl() + { + $this->acl = null; + parent::unsAcl(); + return $this->unsUserAclData(); + } + + /** + * @inheritDoc + */ + public function writeClose() + { + //Updating data in session in case these objects has been changed. + if ($this->user) { + $this->setUser($this->user); + } + if ($this->acl) { + $this->setAcl($this->acl); + } + + parent::writeClose(); + } } diff --git a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php new file mode 100644 index 0000000000000..34e01be696672 --- /dev/null +++ b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php @@ -0,0 +1,36 @@ + $acl->_rules, 'resources' => $acl->_resources, 'roles' => $acl->_roleRegistry]; + } + + /** + * @inheritDoc + */ + public function hydrate(Acl $target, array $data): void + { + $target->_rules = $data['rules']; + $target->_resources = $data['resources']; + $target->_roleRegistry = $data['roles']; + } +} diff --git a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php new file mode 100644 index 0000000000000..6dee8b7b302c8 --- /dev/null +++ b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php @@ -0,0 +1,54 @@ +roleFactory = $roleFactory; + } + + /** + * @inheritDoc + */ + public function extract(User $user): array + { + return ['data' => $user->getData(), 'role_data' => $user->getRole()->getData()]; + } + + /** + * @inheritDoc + */ + public function hydrate(User $target, array $data): void + { + $target->setData($data['data']); + /** @var Role $role */ + $role = $this->roleFactory->create(); + $role->setData($data['role_data']); + $target->setData('extracted_role', $role); + $target->getRole(); + } +} diff --git a/app/code/Magento/Backend/Model/Locale/Resolver.php b/app/code/Magento/Backend/Model/Locale/Resolver.php index b9be471cd5990..9086e2af83e24 100644 --- a/app/code/Magento/Backend/Model/Locale/Resolver.php +++ b/app/code/Magento/Backend/Model/Locale/Resolver.php @@ -7,8 +7,10 @@ /** * Backend locale model + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Resolver extends \Magento\Framework\Locale\Resolver { @@ -40,7 +42,7 @@ class Resolver extends \Magento\Framework\Locale\Resolver * @param Manager $localeManager * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\Validator\Locale $localeValidator - * @param null $locale + * @param string|null $locale * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -76,7 +78,7 @@ public function setLocale($locale = null) $sessionLocale = $this->_session->getSessionLocale(); $userLocale = $this->_localeManager->getUserInterfaceLocale(); - $localeCodes = array_filter([$forceLocale, $sessionLocale, $userLocale]); + $localeCodes = array_filter([$forceLocale, $locale, $sessionLocale, $userLocale]); if (count($localeCodes)) { $locale = reset($localeCodes); diff --git a/app/code/Magento/Backend/README.md b/app/code/Magento/Backend/README.md index 03c7d86516b92..205051809328a 100644 --- a/app/code/Magento/Backend/README.md +++ b/app/code/Magento/Backend/README.md @@ -1,3 +1,112 @@ -The Backend module contains common infrastructure and assets for other modules to be defined and used in their -administration user interface (UI). It does not contain anything specific to other modules. Among many things it -handles the logic of authenticating and authorizing users. +# Magento_Backend module + +The Magento_Backend module contains common infrastructure and assets for other modules to be defined and used in their +administration user interface (UI). + +The Magento_Backend module does not contain anything specific to other modules. Among many things it handles the logic of authenticating and authorizing users. + +## Installation details + +Before disabling or uninstalling this module, note that the following modules depends on this module: + +- Magento_Analytics +- Magento_Authorization +- Magento_NewRelicReporting +- Magento_ProductVideo +- Magento_ReleaseNotification +- Magento_Search +- Magento_Security +- Magento_Signifyd +- Magento_Swatches +- Magento_Ui +- Magento_User +- Magento_Webapi + +For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html). + +## Structure + +Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.3/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `Service/V1`. + +`Service/V1` - contains logic to provide a list of modules installed in Magento. + +For information about typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure). + +## Extensibility + +Extension developers can interact with the Magento_Backend module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backend module. + +### Events + +The module dispatches the following events: + + - `adminhtml_block_html_before` event in the `\Magento\Backend\Block\Template::_toHtml()` method. Parameters: + - `block` is the backend block template (this) (`\Magento\Backend\Block\Template` class). + - `adminhtml_store_edit_form_prepare_form` event in the `\Magento\Backend\Block\System\Store\Edit\AbstractForm::_prepareForm()` method. Parameters: + - `block` is the AbstractForm block (this) (`\Magento\Backend\Block\System\Store\Edit\AbstractForm` class). + - `backend_block_widget_grid_prepare_grid_before` event in the `\Magento\Backend\Block\Widget\Grid::_prepareGrid()` method. Parameters: + - `grid` is the widget grid block (this) (`\Magento\Backend\Block\Widget\Grid` class) + - `collection` is the grid collection (`\Magento\Framework\Data\Collection` class). + - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Console\Command\CacheCleanCommand::performAction()` method. + - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Console\Command\CacheFlushCommand::performAction()` method. + - `clean_catalog_images_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanImages::execute()` method. + - `clean_media_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanMedia::execute()` method. + - `clean_static_files_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanStaticFiles::execute()` method. + - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushAll::execute()` method. + - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushSystem::execute()` method. + - `theme_save_after` event in the `\Magento\Backend\Controller\Adminhtml\System\Design\Save::execute()` method. + - `backend_auth_user_login_success` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters: + - `user` is the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`) + - `backend_auth_user_login_failed` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters: + - `user_name` is username extracted from the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`) + - `exception` any exception generated (`\Magento\Framework\Exception\LocalizedException | \Magento\Framework\Exception\Plugin\AuthenticationException`) + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `admin_login` +- `adminhtml_auth_login` +- `adminhtml_cache_block` +- `adminhtml_cache_index` +- `adminhtml_dashboard_customersmost` +- `adminhtml_dashboard_customersnewest` +- `adminhtml_dashboard_index` +- `adminhtml_dashboard_productsviewed` +- `adminhtml_denied` +- `adminhtml_noroute` +- `adminhtml_system_account_index` +- `adminhtml_system_design_edit` +- `adminhtml_system_design_grid` +- `adminhtml_system_design_grid_block` +- `adminhtml_system_design_index` +- `adminhtml_system_store_deletestore` +- `adminhtml_system_store_editstore` +- `adminhtml_system_store_grid_block` +- `adminhtml_system_store_index` +- `default` +- `editor` +- `empty` +- `formkey` +- `overlay_popup` +- `popup` + + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +### UI components + +You can extend Magento_Backend module using the following configuration files: + +- `view/adminhtml/ui_component/design_config_form.xml` +- `view/adminhtml/ui_component/design_config_listing.xml` + +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php new file mode 100644 index 0000000000000..7227cc92fcc8e --- /dev/null +++ b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php @@ -0,0 +1,34 @@ + + + + + + general/store_information/name + New Store Information + + + general/store_information/phone + + + general/store_information/country_id + + + general/store_information/city + + + general/store_information/postcode + + + general/store_information/street_line1 + + + general/store_information/street_line2 + + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml index a460aaebf1051..3ad8adf9e1b96 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -11,6 +11,9 @@
+ + +
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml new file mode 100644 index 0000000000000..59a6a7e261b87 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml @@ -0,0 +1,37 @@ + + + + + + + + + <description value="Check locale dropdown and developer configuration page are available in developer mode"/> + <group value="backend"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20374"/> + <group value="developer_mode_only"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to the general configuration and make sure the locale dropdown is available and enabled --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfigPage" /> + <scrollTo selector="{{LocaleOptionsSection.sectionHeader}}" stepKey="scrollToLocaleSection" x="0" y="-80" /> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <seeElement selector="{{LocaleOptionsSection.localeEnabled}}" stepKey="seeEnabledLocaleDropdown"/> + + <!-- Go to the developer configuration and make sure the page is available --> + <actionGroup ref="AdminOpenStoreConfigDeveloperPageActionGroup" stepKey="goToDeveloperConfigPage"/> + <seeInCurrentUrl url="{{AdminConfigDeveloperPage.url}}" stepKey="seeDeveloperConfigUrl"/> + <seeElement selector="{{AdminConfigSection.navItemByTitle('Developer')}}" stepKey="assertDeveloperNavItemPresent" /> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml new file mode 100644 index 0000000000000..2dade727ca411 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckLocaleAndDeveloperConfigInProductionModeTest"> + <annotations> + <features value="Backend"/> + <title value="Check locale dropdown and developer configuration page are not available in production mode"/> + <description value="Check locale dropdown and developer configuration page are not available in production mode"/> + <testCaseId value="MC-14106" /> + <severity value="MAJOR"/> + <group value="backend"/> + <group value="production_mode_only"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to the general configuration and make sure the locale dropdown is disabled --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfigPage" /> + <scrollTo selector="{{LocaleOptionsSection.sectionHeader}}" stepKey="scrollToLocaleSection" x="0" y="-80" /> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <assertElementContainsAttribute selector="{{LocaleOptionsSection.locale}}" attribute="disabled" stepKey="seeDisabledLocaleDropdown" /> + + <!-- Go to the developer configuration and make sure the redirect to the configuration page takes place --> + <actionGroup ref="AdminOpenStoreConfigDeveloperPageActionGroup" stepKey="goToDeveloperConfigPage"/> + <seeInCurrentUrl url="{{AdminConfigPage.url}}index/" stepKey="seeConfigurationIndexUrl"/> + + <actionGroup ref="AdminExpandConfigTabActionGroup" stepKey="expandAdvancedTab"> + <argument name="tabName" value="Advanced" /> + </actionGroup> + <dontSeeElement selector="{{AdminConfigSection.navItemByTitle('Developer')}}" stepKey="assertDeveloperNavItemAbsent" /> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php index e62b73f39241d..51411ce04aac4 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Test class for \Magento\Backend\Block\Widget\Grid\Massaction - */ namespace Magento\Backend\Test\Unit\Block\Widget\Grid; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; use Magento\Framework\Authorization; +use Magento\Framework\Data\Collection\AbstractDb as Collection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +/** + * Test class for \Magento\Backend\Block\Widget\Grid\Massaction + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class MassactionTest extends \PHPUnit\Framework\TestCase { /** @@ -54,6 +59,21 @@ class MassactionTest extends \PHPUnit\Framework\TestCase */ private $visibilityCheckerMock; + /** + * @var Collection|\PHPUnit\Framework\MockObject\MockObject + */ + private $gridCollectionMock; + + /** + * @var Select|\PHPUnit\Framework\MockObject\MockObject + */ + private $gridCollectionSelectMock; + + /** + * @var AdapterInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $connectionMock; + protected function setUp() { $this->_gridMock = $this->getMockBuilder(\Magento\Backend\Block\Widget\Grid::class) @@ -97,6 +117,18 @@ protected function setUp() ->setMethods(['isAllowed']) ->getMock(); + $this->gridCollectionMock = $this->createMock(Collection::class); + $this->gridCollectionSelectMock = $this->createMock(Select::class); + $this->connectionMock = $this->createMock(AdapterInterface::class); + + $this->gridCollectionMock->expects($this->any()) + ->method('getSelect') + ->willReturn($this->gridCollectionSelectMock); + + $this->gridCollectionMock->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $arguments = [ 'layout' => $this->_layoutMock, 'request' => $this->_requestMock, @@ -269,6 +301,41 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } + /** + * Test for getGridIdsJson when select all functionality flag set to true. + */ + public function testGetGridIdsJsonWithUseSelectAll() + { + $this->_block->setUseSelectAll(true); + + $this->_gridMock->expects($this->once()) + ->method('getCollection') + ->willReturn($this->gridCollectionMock); + + $this->gridCollectionSelectMock->expects($this->exactly(4)) + ->method('reset') + ->withConsecutive( + [Select::ORDER], + [Select::LIMIT_COUNT], + [Select::LIMIT_OFFSET], + [Select::COLUMNS] + ); + + $this->gridCollectionSelectMock->expects($this->once()) + ->method('columns') + ->with('test_id'); + + $this->connectionMock->expects($this->once()) + ->method('fetchCol') + ->with($this->gridCollectionSelectMock) + ->willReturn([1, 2, 3]); + + $this->assertEquals( + '1,2,3', + $this->_block->getGridIdsJson() + ); + } + /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php deleted file mode 100644 index f1a4bc355b08e..0000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php +++ /dev/null @@ -1,273 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Auth; - -use Magento\Backend\Model\Auth\Session; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -/** - * Class SessionTest tests Magento\Backend\Model\Auth\Session - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SessionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\App\Config | \PHPUnit_Framework_MockObject_MockObject - */ - protected $config; - - /** - * @var \Magento\Framework\Session\Config | \PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionConfig; - - /** - * @var \Magento\Framework\Stdlib\CookieManagerInterface | \PHPUnit_Framework_MockObject_MockObject - */ - protected $cookieManager; - - /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory | \PHPUnit_Framework_MockObject_MockObject - */ - protected $cookieMetadataFactory; - - /** - * @var \Magento\Framework\Session\Storage | \PHPUnit_Framework_MockObject_MockObject - */ - protected $storage; - - /** - * @var \Magento\Framework\Acl\Builder | \PHPUnit_Framework_MockObject_MockObject - */ - protected $aclBuilder; - - /** - * @var Session - */ - protected $session; - - protected function setUp() - { - $this->cookieMetadataFactory = $this->createPartialMock( - \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class, - ['createPublicCookieMetadata'] - ); - - $this->config = $this->createPartialMock(\Magento\Backend\App\Config::class, ['getValue']); - $this->cookieManager = $this->createPartialMock( - \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class, - ['getCookie', 'setPublicCookie'] - ); - $this->storage = $this->createPartialMock( - \Magento\Framework\Session\Storage::class, - ['getUser', 'getAcl', 'setAcl'] - ); - $this->sessionConfig = $this->createPartialMock( - \Magento\Framework\Session\Config::class, - ['getCookiePath', 'getCookieDomain', 'getCookieSecure', 'getCookieHttpOnly'] - ); - $this->aclBuilder = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class) - ->disableOriginalConstructor() - ->getMock(); - $objectManager = new ObjectManager($this); - $this->session = $objectManager->getObject( - \Magento\Backend\Model\Auth\Session::class, - [ - 'config' => $this->config, - 'sessionConfig' => $this->sessionConfig, - 'cookieManager' => $this->cookieManager, - 'cookieMetadataFactory' => $this->cookieMetadataFactory, - 'storage' => $this->storage, - 'aclBuilder' => $this->aclBuilder - ] - ); - } - - protected function tearDown() - { - $this->config = null; - $this->sessionConfig = null; - $this->session = null; - } - - /** - * @dataProvider refreshAclDataProvider - * @param $isUserPassedViaParams - */ - public function testRefreshAcl($isUserPassedViaParams) - { - $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); - $this->aclBuilder->expects($this->any())->method('getAcl')->willReturn($aclMock); - $userMock = $this->getMockBuilder(\Magento\User\Model\User::class) - ->setMethods(['getReloadAclFlag', 'setReloadAclFlag', 'unsetData', 'save']) - ->disableOriginalConstructor() - ->getMock(); - $userMock->expects($this->any())->method('getReloadAclFlag')->willReturn(true); - $userMock->expects($this->once())->method('setReloadAclFlag')->with('0')->willReturnSelf(); - $userMock->expects($this->once())->method('save'); - $this->storage->expects($this->once())->method('setAcl')->with($aclMock); - $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); - if ($isUserPassedViaParams) { - $this->session->refreshAcl($userMock); - } else { - $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); - $this->session->refreshAcl(); - } - $this->assertSame($aclMock, $this->session->getAcl()); - } - - /** - * @return array - */ - public function refreshAclDataProvider() - { - return [ - 'User set via params' => [true], - 'User set to session object' => [false] - ]; - } - - public function testIsLoggedInPositive() - { - $user = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', '__wakeup']); - $user->expects($this->once()) - ->method('getId') - ->will($this->returnValue(1)); - - $this->storage->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - - $this->assertTrue($this->session->isLoggedIn()); - } - - public function testProlong() - { - $name = session_name(); - $cookie = 'cookie'; - $lifetime = 900; - $path = '/'; - $domain = 'magento2'; - $secure = true; - $httpOnly = true; - - $this->config->expects($this->once()) - ->method('getValue') - ->with(\Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME) - ->willReturn($lifetime); - $cookieMetadata = $this->createMock(\Magento\Framework\Stdlib\Cookie\PublicCookieMetadata::class); - $cookieMetadata->expects($this->once()) - ->method('setDuration') - ->with($lifetime) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setPath') - ->with($path) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setDomain') - ->with($domain) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setSecure') - ->with($secure) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setHttpOnly') - ->with($httpOnly) - ->will($this->returnSelf()); - - $this->cookieMetadataFactory->expects($this->once()) - ->method('createPublicCookieMetadata') - ->will($this->returnValue($cookieMetadata)); - - $this->cookieManager->expects($this->once()) - ->method('getCookie') - ->with($name) - ->will($this->returnValue($cookie)); - $this->cookieManager->expects($this->once()) - ->method('setPublicCookie') - ->with($name, $cookie, $cookieMetadata); - - $this->sessionConfig->expects($this->once()) - ->method('getCookiePath') - ->will($this->returnValue($path)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieDomain') - ->will($this->returnValue($domain)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieSecure') - ->will($this->returnValue($secure)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieHttpOnly') - ->will($this->returnValue($httpOnly)); - - $this->session->prolong(); - - $this->assertLessThanOrEqual(time(), $this->session->getUpdatedAt()); - } - - /** - * @dataProvider isAllowedDataProvider - * @param bool $isUserDefined - * @param bool $isAclDefined - * @param bool $isAllowed - * @param true $expectedResult - */ - public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expectedResult) - { - $userAclRole = 'userAclRole'; - if ($isAclDefined) { - $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); - $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); - } - if ($isUserDefined) { - $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)->disableOriginalConstructor()->getMock(); - $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); - } - if ($isAclDefined && $isUserDefined) { - $userMock->expects($this->any())->method('getAclRole')->willReturn($userAclRole); - $aclMock->expects($this->once())->method('isAllowed')->with($userAclRole)->willReturn($isAllowed); - } - - $this->assertEquals($expectedResult, $this->session->isAllowed('resource')); - } - - /** - * @return array - */ - public function isAllowedDataProvider() - { - return [ - "Negative: User not defined" => [false, true, true, false], - "Negative: Acl not defined" => [true, false, true, false], - "Negative: Permission denied" => [true, true, false, false], - "Positive: Permission granted" => [true, true, false, false], - ]; - } - - /** - * @dataProvider firstPageAfterLoginDataProvider - * @param bool $isFirstPageAfterLogin - */ - public function testFirstPageAfterLogin($isFirstPageAfterLogin) - { - $this->session->setIsFirstPageAfterLogin($isFirstPageAfterLogin); - $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin()); - } - - /** - * @return array - */ - public function firstPageAfterLoginDataProvider() - { - return [ - 'First page after login' => [true], - 'Not first page after login' => [false], - ]; - } -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php deleted file mode 100644 index 77c428a6a116a..0000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Authorization; - -/** - * Class RoleLocatorTest - */ -class RoleLocatorTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\Model\Authorization\RoleLocator - */ - protected $_model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sessionMock = []; - - protected function setUp() - { - $this->_sessionMock = $this->createPartialMock( - \Magento\Backend\Model\Auth\Session::class, - ['getUser', 'getAclRole', 'hasUser'] - ); - $this->_model = new \Magento\Backend\Model\Authorization\RoleLocator($this->_sessionMock); - } - - public function testGetAclRoleIdReturnsCurrentUserAclRoleId() - { - $this->_sessionMock->expects($this->once())->method('hasUser')->will($this->returnValue(true)); - $this->_sessionMock->expects($this->once())->method('getUser')->will($this->returnSelf()); - $this->_sessionMock->expects($this->once())->method('getAclRole')->will($this->returnValue('some_role')); - $this->assertEquals('some_role', $this->_model->getAclRoleId()); - } -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php deleted file mode 100644 index ce2b65a2249ac..0000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Locale; - -use Magento\Framework\Locale\Resolver; - -/** - * Class ManagerTest - */ -class ManagerTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\Model\Locale\Manager - */ - protected $_model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\TranslateInterface - */ - protected $_translator; - - /** - * @var \Magento\Backend\Model\Session - */ - protected $_session; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Auth\Session - */ - protected $_authSession; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\App\ConfigInterface - */ - protected $_backendConfig; - - protected function setUp() - { - $this->_session = $this->createMock(\Magento\Backend\Model\Session::class); - - $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']); - - $this->_backendConfig = $this->getMockForAbstractClass( - \Magento\Backend\App\ConfigInterface::class, - [], - '', - false - ); - - $userMock = new \Magento\Framework\DataObject(); - - $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock)); - - $this->_translator = $this->getMockBuilder(\Magento\Framework\TranslateInterface::class) - ->setMethods(['init', 'setLocale']) - ->getMockForAbstractClass(); - - $this->_translator->expects($this->any())->method('setLocale')->will($this->returnValue($this->_translator)); - - $this->_translator->expects($this->any())->method('init')->will($this->returnValue(false)); - - $this->_model = new \Magento\Backend\Model\Locale\Manager( - $this->_session, - $this->_authSession, - $this->_translator, - $this->_backendConfig - ); - } - - /** - * @return array - */ - public function switchBackendInterfaceLocaleDataProvider() - { - return ['case1' => ['locale' => 'de_DE'], 'case2' => ['locale' => 'en_US']]; - } - - /** - * @param string $locale - * @dataProvider switchBackendInterfaceLocaleDataProvider - * @covers \Magento\Backend\Model\Locale\Manager::switchBackendInterfaceLocale - */ - public function testSwitchBackendInterfaceLocale($locale) - { - $this->_model->switchBackendInterfaceLocale($locale); - - $userInterfaceLocale = $this->_authSession->getUser()->getInterfaceLocale(); - $this->assertEquals($userInterfaceLocale, $locale); - - $sessionLocale = $this->_session->getSessionLocale(); - $this->assertEquals($sessionLocale, null); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceLocaleDefault() - { - $locale = $this->_model->getUserInterfaceLocale(); - - $this->assertEquals($locale, Resolver::DEFAULT_LOCALE); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceLocale() - { - $this->_model->switchBackendInterfaceLocale('de_DE'); - $locale = $this->_model->getUserInterfaceLocale(); - - $this->assertEquals($locale, 'de_DE'); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceGeneralLocale() - { - $this->_backendConfig->expects($this->any()) - ->method('getValue') - ->with('general/locale/code') - ->willReturn('test_locale'); - $locale = $this->_model->getUserInterfaceLocale(); - $this->assertEquals($locale, 'test_locale'); - } -} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index 4862e701404f7..5a7884a9607fe 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -22,6 +22,7 @@ "magento/module-store": "*", "magento/module-translation": "*", "magento/module-ui": "*", + "magento/module-authorization": "*", "magento/module-user": "*" }, "suggest": { diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 343ecc0ee3d58..d3b1cbaf7a5cc 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -323,7 +323,8 @@ </field> <field id="port" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Port (25)</label> - <comment>For Windows server only.</comment> + <validate>validate-digits validate-digits-range digits-range-0-65535</validate> + <comment>Please enter at least 0 and at most 65535 (For Windows server only).</comment> </field> <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Set Return-Path</label> @@ -481,22 +482,22 @@ <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}} placeholder.</comment> + <comment><![CDATA[Specify URL or {{base_url}} placeholder.]]></comment> </field> <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Base Link URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May start with {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May start with {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_static_url" translate="label comment" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL for Static View Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May be empty or start with {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Base URL for User Media Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May be empty or start with {{unsecure_base_url}} placeholder.]]></comment> </field> </group> <group id="secure" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -505,22 +506,22 @@ <field id="base_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[Specify URL or {{base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_link_url" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Secure Base Link URL</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May start with {{secure_base_url}} or {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_static_url" translate="label comment" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL for Static View Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="base_media_url" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Secure Base URL for User Media Files</label> <backend_model>Magento\Config\Model\Config\Backend\Baseurl</backend_model> - <comment>May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.</comment> + <comment><![CDATA[May be empty or start with {{secure_base_url}}, or {{unsecure_base_url}} placeholder.]]></comment> </field> <field id="use_in_frontend" translate="label comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Use Secure URLs on Storefront</label> diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index c526703da9975..41db85b9323a8 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -198,4 +198,8 @@ <argument name="anchorRenderer" xsi:type="object">Magento\Backend\Block\AnchorRenderer</argument> </arguments> </type> + <preference for="Magento\Backend\Spi\SessionUserHydratorInterface" + type="Magento\Backend\Model\Auth\SessionUserHydrator" /> + <preference for="Magento\Backend\Spi\SessionAclHydratorInterface" + type="Magento\Backend\Model\Auth\SessionAclHydrator" /> </config> diff --git a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js index ae0e84e2d27f8..e886f28cd158b 100644 --- a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - 'mediaUploader': 'Magento_Backend/js/media-uploader' + 'mediaUploader': 'Magento_Backend/js/media-uploader', + 'mage/translate': 'Magento_Backend/js/translate' } } }; diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index b712bc6c95315..7f6f2bbd13fa5 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -170,6 +170,9 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; <?php if ($block->getSortableUpdateCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = <?= /* @noEscape */ $block->getSortableUpdateCallback() ?>; <?php endif; ?> + <?php if ($block->getFilterKeyPressCallback()) : ?> + <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; + <?php endif; ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.bindSortable(); <?php if ($block->getRowInitCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml index 0bb453f25d7ca..527ddc436207f 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml @@ -272,6 +272,9 @@ $numColumns = count($block->getColumns()); <?php if ($block->getCheckboxCheckCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; <?php endif; ?> + <?php if ($block->getFilterKeyPressCallback()) : ?> + <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; + <?php endif; ?> <?php if ($block->getRowInitCallback()) : ?> <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/translate.js b/app/code/Magento/Backend/view/adminhtml/web/js/translate.js new file mode 100644 index 0000000000000..d6e1547600c4e --- /dev/null +++ b/app/code/Magento/Backend/view/adminhtml/web/js/translate.js @@ -0,0 +1,48 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable strict */ +define([ + 'jquery', + 'mage/mage' +], function ($) { + $.extend(true, $, { + mage: { + translate: (function () { + /** + * Key-value translations storage + * @type {Object} + * @private + */ + var _data = {}; + + /** + * Add new translation (two string parameters) or several translations (object) + */ + this.add = function () { + if (arguments.length > 1) { + _data[arguments[0]] = arguments[1]; + } else if (typeof arguments[0] === 'object') { + $.extend(_data, arguments[0]); + } + }; + + /** + * Make a translation with parsing (to handle case when _data represents tuple) + * @param {String} text + * @return {String} + */ + this.translate = function (text) { + return _data[text] ? _data[text] : text; + }; + + return this; + }()) + } + }); + $.mage.__ = $.proxy($.mage.translate.translate, $.mage.translate); + + return $.mage.__; +}); diff --git a/app/code/Magento/Backup/README.md b/app/code/Magento/Backup/README.md index 59688ea3e716e..e1167bc4f2429 100644 --- a/app/code/Magento/Backup/README.md +++ b/app/code/Magento/Backup/README.md @@ -1,3 +1,28 @@ -The Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups. +# Magento_Backup module -This module does not affect the storefront. +The Magento_Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups. + +The Magento_Backup module does not affect the storefront. + +For more information about this module, see [Magento Backups](https://docs.magento.com/m2/ce/user_guide/system/backups.html) + +## Extensibility + +Extension developers can interact with the Magento_Backup module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backup module. + +### Layouts + +This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory: + +`backup_index_block` +`backup_index_disabled` +`backup_index_grid` +`backup_index_index` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php index 58ce33305da85..2f73dd8f380dc 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -11,6 +11,7 @@ use Braintree\Error\Validation; use Braintree\Result\Error; use Braintree\Result\Successful; +use Braintree\Transaction; /** * Processes errors codes from Braintree response. @@ -38,12 +39,14 @@ public function getErrorCodes($response): array $result[] = $error->code; } - if (isset($response->transaction) && $response->transaction->status === 'gateway_rejected') { - $result[] = $response->transaction->gatewayRejectionReason; - } + if (isset($response->transaction) && $response->transaction) { + if ($response->transaction->status === Transaction::GATEWAY_REJECTED) { + $result[] = $response->transaction->gatewayRejectionReason; + } - if (isset($response->transaction) && $response->transaction->status === 'processor_declined') { - $result[] = $response->transaction->processorResponseCode; + if ($response->transaction->status === Transaction::PROCESSOR_DECLINED) { + $result[] = $response->transaction->processorResponseCode; + } } return $result; diff --git a/app/code/Magento/Braintree/README.md b/app/code/Magento/Braintree/README.md index 8c34b7ae1af67..66d872e55a21a 100644 --- a/app/code/Magento/Braintree/README.md +++ b/app/code/Magento/Braintree/README.md @@ -1 +1,47 @@ -Module Magento\Braintree implements integration with the Braintree payment system. \ No newline at end of file +# Magento_Braintree module + +The Magento_Braintree module implements integration with the Braintree payment system. + +## Extensibility + +Extension developers can interact with the Magento_Braintree module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Braintree module. + +### Events + +This module observes the following events: + + - `payment_method_assign_data_braintree` event in `Magento\Braintree\Observer\DataAssignObserver` file. + - `payment_method_assign_data_braintree_paypal` event in `Magento\Braintree\Observer\DataAssignObserver` file. + - `shortcut_buttons_container` event in `Magento\Braintree\Observer\AddPaypalShortcuts` file. + +For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). + +### Layouts + +This module interacts with the following layouts and layout handles in the `view/adminhtml/layout` directory: + +- `braintree_paypal_review` +- `checkout_index_index` +- `multishipping_checkout_billing` +- `vault_cards_listaction` + +This module interacts with the following layout handles in the `view/frontend/layout` directory: + +- `adminhtml_system_config_edit` +- `braintree_report_index` +- `sales_order_create_index` +- `sales_order_create_load_block_billing_method` + +For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). + +### UI components + +You can extend admin notifications using the `view/adminhtml/ui_component/braintree_report.xml` configuration file. + +For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index adfa58ef22ef3..ea5200e4ba51f 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -336,7 +336,7 @@ define([ } return { - line1: address.street[0], + line1: _.isUndefined(address.street) || _.isUndefined(address.street[0]) ? '' : address.street[0], city: address.city, state: address.regionCode, postalCode: address.postcode, diff --git a/app/code/Magento/BraintreeGraphQl/README.md b/app/code/Magento/BraintreeGraphQl/README.md index f6740e4d250e9..4e8eecc93a924 100644 --- a/app/code/Magento/BraintreeGraphQl/README.md +++ b/app/code/Magento/BraintreeGraphQl/README.md @@ -1,4 +1,9 @@ -# BraintreeGraphQl +# Magento_BraintreeGraphQl module -**BraintreeGraphQl** provides type and resolver for method additional -information. \ No newline at end of file +The Magento_BraintreeGraphQl module provides type and resolver information for the GraphQL module to pass payment information data from the client to Magento. + +## Extensibility + +Extension developers can interact with the Magento_BraintreeGraphQl module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_BraintreeGraphQl module. diff --git a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls index 0492f8aaf989b..08bd10fd4c2dd 100644 --- a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. type Mutation { - createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Braintree Client Token for creating client-side nonce.") + createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Client Token for Braintree Javascript SDK initialization.") } input PaymentMethodInput { @@ -11,9 +11,9 @@ input PaymentMethodInput { } input BraintreeInput { - payment_method_nonce: String! - is_active_payment_token_enabler: Boolean! - device_data: String + payment_method_nonce: String! @doc(description:"The one-time payment token generated by Braintree payment gateway based on card details. Required field to make sale transaction.") + is_active_payment_token_enabler: Boolean! @doc(description:"States whether an entered by a customer credit/debit card should be tokenized for later usage. Required only if Vault is enabled for Braintree payment integration.") + device_data: String @doc(description:"Contains a fingerprint provided by Braintree JS SDK and should be sent with sale transaction details to the Braintree payment gateway. Should be specified only in a case if Kount (advanced fraud protection) is enabled for Braintree payment integration.") } input BraintreeCcVaultInput { diff --git a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php index 61559df4d2cf6..ecbf4cc80a3ae 100644 --- a/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php +++ b/app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type; +/** + * Provides duplicating bundle options and selections + */ class Bundle implements \Magento\Catalog\Model\Product\CopyConstructorInterface { /** @@ -27,7 +30,17 @@ public function build(Product $product, Product $duplicate) $bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: []; $duplicatedBundleOptions = []; foreach ($bundleOptions as $key => $bundleOption) { - $duplicatedBundleOptions[$key] = clone $bundleOption; + $duplicatedBundleOption = clone $bundleOption; + /** + * Set option and selection ids to 'null' in order to create new option(selection) for duplicated product, + * but not modifying existing one, which led to lost of option(selection) in original product. + */ + $productLinks = $duplicatedBundleOption->getProductLinks() ?: []; + foreach ($productLinks as $productLink) { + $productLink->setSelectionId(null); + } + $duplicatedBundleOption->setOptionId(null); + $duplicatedBundleOptions[$key] = $duplicatedBundleOption; } $duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions); } diff --git a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index c6a67cc5a110c..0000000000000 --- a/app/code/Magento/Bundle/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Bundle\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tables = [ - 'catalog_product_index_price_bundle_tmp', - 'catalog_product_index_price_bundle_sel_tmp', - 'catalog_product_index_price_bundle_opt_tmp', - ]; - foreach ($tables as $table) { - $tableName = $this->schemaSetup->getTable($table); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index 6e7e4a7a16573..e5f557dd22ded 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -61,6 +61,20 @@ <requiredEntity type="custom_attribute">CustomAttributeDynamicPrice</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> </entity> + <entity name="ApiBundleProductUnderscoredSku" type="product2"> + <data key="name" unique="suffix">Api Bundle Product</data> + <data key="sku" unique="suffix">api_bundle_product</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-bundle-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeDynamicPrice</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> + </entity> <entity name="ApiBundleProductPriceViewRange" type="product2"> <data key="name" unique="suffix">Api Bundle Product</data> <data key="sku" unique="suffix">api-bundle-product</data> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index 1438958b92b61..730df90b31be6 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -24,6 +24,10 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> </before> <after> + <!-- Delete the bundled product we created in the test body --> + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml index 52bce67600888..c6aab0ea54ea2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml @@ -17,6 +17,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-139"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByNameMysqlTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product name using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product name using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20472"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -56,7 +97,7 @@ <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> - <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="ApiBundleProductUnderscoredSku" stepKey="product"/> <createData entity="DropDownBundleOption" stepKey="bundleOption"> <requiredEntity createDataKey="product"/> </createData> @@ -87,6 +128,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-242"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20473"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -122,6 +204,47 @@ <severity value="MAJOR"/> <testCaseId value="MC-250"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByShortDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product short description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product short description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20474"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -157,6 +280,56 @@ <severity value="MAJOR"/> <testCaseId value="MC-251"/> <group value="Bundle"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <getData entity="GetProduct" stepKey="arg1"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg2"> + <requiredEntity createDataKey="simple1"/> + </getData> + <getData entity="GetProduct" stepKey="arg3"> + <requiredEntity createDataKey="simple2"/> + </getData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchBundleByPriceMysqlTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Bundle product with product price using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Bundle product with product price the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20475"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..d8d6034cd1a21 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Bundle"/> + <stories value="Advanced Catalog Product Search for all product types "/> + <title value="Guest customer should be able to advance search Bundle product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search Bundle product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20359"/> + <group value="Bundle"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="simple2"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php index 831098cc44c38..4df60d07d98ef 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor; use Magento\Bundle\Api\Data\BundleOptionInterface; +use Magento\Bundle\Model\Link; use Magento\Bundle\Model\Product\CopyConstructor\Bundle; use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; @@ -45,6 +46,7 @@ public function testBuildNegative() */ public function testBuildPositive() { + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); @@ -60,18 +62,42 @@ public function testBuildPositive() ->method('getExtensionAttributes') ->willReturn($extensionAttributesProduct); + $productLink = $this->getMockBuilder(Link::class) + ->setMethods(['setSelectionId']) + ->disableOriginalConstructor() + ->getMock(); + $productLink->expects($this->exactly(2)) + ->method('setSelectionId') + ->with($this->identicalTo(null)); + $firstOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $firstOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $firstOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); + $secondOption = $this->getMockBuilder(BundleOptionInterface::class) + ->setMethods(['getProductLinks']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $secondOption->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$productLink]); + $secondOption->expects($this->once()) + ->method('setOptionId') + ->with($this->identicalTo(null)); $bundleOptions = [ - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(), - $this->getMockBuilder(BundleOptionInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass() + $firstOption, + $secondOption ]; $extensionAttributesProduct->expects($this->once()) ->method('getBundleProductOptions') ->willReturn($bundleOptions); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $duplicate */ $duplicate = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Bundle/etc/db_schema.xml b/app/code/Magento/Bundle/etc/db_schema.xml index 97e86e5c17359..dba9732439065 100644 --- a/app/code/Magento/Bundle/etc/db_schema.xml +++ b/app/code/Magento/Bundle/etc/db_schema.xml @@ -10,9 +10,9 @@ <table name="catalog_product_bundle_option" resource="default" engine="innodb" comment="Catalog Product Bundle Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="smallint" name="required" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Required"/> <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" @@ -31,14 +31,14 @@ <table name="catalog_product_bundle_option_value" resource="default" engine="innodb" comment="Catalog Product Bundle Option Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> @@ -54,13 +54,13 @@ <table name="catalog_product_bundle_selection" resource="default" engine="innodb" comment="Catalog Product Bundle Selection"> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Selection Id"/> + comment="Selection ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Position"/> <column xsi:type="smallint" name="is_default" padding="5" unsigned="true" nullable="false" identity="false" @@ -92,15 +92,15 @@ <table name="catalog_product_bundle_selection_price" resource="default" engine="innodb" comment="Catalog Product Bundle Selection Price"> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Selection Id"/> + comment="Selection ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="selection_price_type" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Selection Price Type"/> <column xsi:type="decimal" name="selection_price_value" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Selection Price Value"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Product Id"/> + comment="Parent Product ID"/> <constraint xsi:type="primary" referenceId="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> <column name="selection_id"/> <column name="parent_product_id"/> @@ -122,7 +122,7 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Customer Group ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="false" @@ -159,7 +159,7 @@ <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="smallint" name="stock_status" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Stock Status"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -203,7 +203,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -246,9 +246,9 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Selection Id"/> + default="0" comment="Selection ID"/> <column xsi:type="smallint" name="group_type" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Group Type"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -265,7 +265,7 @@ <column name="selection_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Sel Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -274,9 +274,9 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Selection Id"/> + default="0" comment="Selection ID"/> <column xsi:type="smallint" name="group_type" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Group Type"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -302,7 +302,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" comment="Min Price"/> <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" @@ -320,7 +320,7 @@ <column name="option_id"/> </constraint> </table> - <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Opt Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -329,7 +329,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="decimal" name="min_price" scale="6" precision="20" unsigned="false" nullable="true" comment="Min Price"/> <column xsi:type="decimal" name="alt_price" scale="6" precision="20" unsigned="false" nullable="true" diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..fead6f923d8f0 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Product\Price; + +use Magento\Bundle\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Bundle\Model\Product\Price; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides pricing information for Bundle products + */ +class Provider implements ProviderInterface +{ + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + if ($product->getPriceType() == Price::PRICE_TYPE_FIXED) { + return $product->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getAmount(); + } + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index 50a2e32b8c9d5..98dbe012c9002 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -40,4 +40,22 @@ </argument> </arguments> </type> + + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\BundleGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\AttributeProcessor"> + <arguments> + <argument name="fieldToAttributeMap" xsi:type="array"> + <item name="price_range" xsi:type="array"> + <item name="price_type" xsi:type="string">price_type</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php index db42bb66c9bd1..60e17599f6dec 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php @@ -11,15 +11,32 @@ class Back extends Generic { /** + * Get Button Data + * * @return array */ public function getButtonData() { return [ 'label' => __('Back'), - 'on_click' => sprintf("location.href = '%s';", $this->getUrl('*/*/')), + 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()), 'class' => 'back', 'sort_order' => 10 ]; } + /** + * Get URL for back + * + * @return string + */ + private function getBackUrl() + { + if ($this->context->getRequestParam('customerId')) { + return $this->getUrl( + 'customer/index/edit', + ['id' => $this->context->getRequestParam('customerId')] + ); + } + return $this->getUrl('*/*/'); + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php index 733e270174e4c..8af59dfeaf76a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php @@ -6,31 +6,41 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\Controller\ResultFactory; +use Magento\Backend\Model\View\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Catalog\Controller\Adminhtml\Category; +use Magento\Backend\Model\View\Result\ForwardFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; /** * Class Add Category * * @package Magento\Catalog\Controller\Adminhtml\Category */ -class Add extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface +class Add extends Category implements HttpGetActionInterface { /** * Forward factory for result * - * @var \Magento\Backend\Model\View\Result\ForwardFactory + * @deprecated Unused Class: ForwardFactory + * @see $this->resultFactory->create() + * @var ForwardFactory + * */ protected $resultForwardFactory; /** * Add category constructor * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param Context $context + * @param ForwardFactory $resultForwardFactory */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + Context $context, + ForwardFactory $resultForwardFactory ) { parent::__construct($context); $this->resultForwardFactory = $resultForwardFactory; @@ -39,7 +49,7 @@ public function __construct( /** * Add new category form * - * @return \Magento\Backend\Model\View\Result\Forward + * @return ResultInterface */ public function execute() { @@ -47,7 +57,7 @@ public function execute() $category = $this->_initCategory(true); if (!$category || !$parentId || $category->getId()) { - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]); } @@ -61,9 +71,8 @@ public function execute() $category->addData($categoryData); } - $resultPageFactory = $this->_objectManager->get(\Magento\Framework\View\Result\PageFactory::class); - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ - $resultPage = $resultPageFactory->create(); + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); if ($this->getRequest()->getQuery('isAjax')) { return $this->ajaxRequestResponse($category, $resultPage); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php index c31ceabcda655..eb1176f787c61 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php @@ -4,10 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +/** + * Edit product + */ class Edit extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** @@ -51,7 +55,7 @@ public function execute() $productId = (int) $this->getRequest()->getParam('id'); $product = $this->productBuilder->build($this->getRequest()); - if (($productId && !$product->getEntityId())) { + if ($productId && !$product->getEntityId()) { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $this->messageManager->addErrorMessage(__('This product doesn\'t exist.')); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php index 6f6870cb0849f..89d2c1b8a066e 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php @@ -6,45 +6,63 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\Registry; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\Action\HttpGetActionInterface; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface +/** + * Edit attribute set controller. + */ +class Edit extends Set implements HttpGetActionInterface { /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @var AttributeSetRepositoryInterface + */ + private $attributeSetRepository; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param PageFactory $resultPageFactory + * @param AttributeSetRepositoryInterface $attributeSetRepository */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\View\Result\PageFactory $resultPageFactory + Context $context, + Registry $coreRegistry, + PageFactory $resultPageFactory, + AttributeSetRepositoryInterface $attributeSetRepository = null ) { parent::__construct($context, $coreRegistry); $this->resultPageFactory = $resultPageFactory; + $this->attributeSetRepository = $attributeSetRepository ?: + ObjectManager::getInstance()->get(AttributeSetRepositoryInterface::class); } /** - * @return \Magento\Backend\Model\View\Result\Page + * @inheritdoc */ public function execute() { $this->_setTypeId(); - $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class) - ->load($this->getRequest()->getParam('id')); - + $attributeSet = $this->attributeSetRepository->get($this->getRequest()->getParam('id')); if (!$attributeSet->getId()) { return $this->resultRedirectFactory->create()->setPath('catalog/*/index'); } - $this->_coreRegistry->register('current_attribute_set', $attributeSet); - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ + /** @var Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $resultPage->setActiveMenu('Magento_Catalog::catalog_attributes_sets'); $resultPage->getConfig()->getTitle()->prepend(__('Attribute Sets')); diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php index 33e261dc353b4..93b67965e7234 100644 --- a/app/code/Magento/Catalog/Helper/Output.php +++ b/app/code/Magento/Catalog/Helper/Output.php @@ -9,9 +9,21 @@ use Magento\Catalog\Model\Category as ModelCategory; use Magento\Catalog\Model\Product as ModelProduct; +use Magento\Eav\Model\Config; +use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filter\Template; +use function is_object; +use function method_exists; +use function preg_match; +use function strtolower; -class Output extends \Magento\Framework\App\Helper\AbstractHelper +/** + * Html output + */ +class Output extends AbstractHelper { /** * Array of existing handlers @@ -37,12 +49,12 @@ class Output extends \Magento\Framework\App\Helper\AbstractHelper /** * Eav config * - * @var \Magento\Eav\Model\Config + * @var Config */ protected $_eavConfig; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $_escaper; @@ -53,27 +65,32 @@ class Output extends \Magento\Framework\App\Helper\AbstractHelper /** * Output constructor. - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Eav\Model\Config $eavConfig + * @param Context $context + * @param Config $eavConfig * @param Data $catalogData - * @param \Magento\Framework\Escaper $escaper + * @param Escaper $escaper * @param array $directivePatterns + * @param array $handlers */ public function __construct( - \Magento\Framework\App\Helper\Context $context, - \Magento\Eav\Model\Config $eavConfig, + Context $context, + Config $eavConfig, Data $catalogData, - \Magento\Framework\Escaper $escaper, - $directivePatterns = [] + Escaper $escaper, + $directivePatterns = [], + array $handlers = [] ) { $this->_eavConfig = $eavConfig; $this->_catalogData = $catalogData; $this->_escaper = $escaper; $this->directivePatterns = $directivePatterns; + $this->_handlers = $handlers; parent::__construct($context); } /** + * Return template processor + * * @return Template */ protected function _getTemplateProcessor() @@ -115,8 +132,7 @@ public function addHandler($method, $handler) */ public function getHandlers($method) { - $method = strtolower($method); - return $this->_handlers[$method] ?? []; + return $this->_handlers[strtolower($method)] ?? []; } /** @@ -145,21 +161,21 @@ public function process($method, $result, $params) * @param string $attributeName * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function productAttribute($product, $attributeHtml, $attributeName) { $attribute = $this->_eavConfig->getAttribute(ModelProduct::ENTITY, $attributeName); if ($attribute && $attribute->getId() && - $attribute->getFrontendInput() != 'media_image' && + $attribute->getFrontendInput() !== 'media_image' && (!$attribute->getIsHtmlAllowedOnFront() && !$attribute->getIsWysiwygEnabled()) ) { - if ($attribute->getFrontendInput() != 'price') { + if ($attribute->getFrontendInput() !== 'price') { $attributeHtml = $this->_escaper->escapeHtml($attributeHtml); } - if ($attribute->getFrontendInput() == 'textarea') { + if ($attribute->getFrontendInput() === 'textarea') { $attributeHtml = nl2br($attributeHtml); } } @@ -187,14 +203,14 @@ public function productAttribute($product, $attributeHtml, $attributeName) * @param string $attributeHtml * @param string $attributeName * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function categoryAttribute($category, $attributeHtml, $attributeName) { $attribute = $this->_eavConfig->getAttribute(ModelCategory::ENTITY, $attributeName); if ($attribute && - $attribute->getFrontendInput() != 'image' && + $attribute->getFrontendInput() !== 'image' && (!$attribute->getIsHtmlAllowedOnFront() && !$attribute->getIsWysiwygEnabled()) ) { diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index c96b2aae36059..eeb4c082ff51d 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Category; use Magento\Catalog\Api\Data\CategoryInterface; @@ -20,6 +22,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\Stdlib\ArrayUtils; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Form\Field; @@ -28,10 +31,9 @@ use Magento\Framework\AuthorizationInterface; /** - * Class DataProvider + * Category form data provider. * * @api - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @since 101.0.0 @@ -52,6 +54,7 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider /** * EAV attribute properties to fetch from meta storage + * * @var array * @since 101.0.0 */ @@ -143,6 +146,11 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider */ private $arrayManager; + /** + * @var ArrayUtils + */ + private $arrayUtils; + /** * @var Filesystem */ @@ -154,8 +162,6 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider private $auth; /** - * DataProvider constructor - * * @param string $name * @param string $primaryFieldName * @param string $requestFieldName @@ -170,6 +176,8 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider * @param array $data * @param PoolInterface|null $pool * @param AuthorizationInterface|null $auth + * @param ArrayUtils|null $arrayUtils + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -186,7 +194,8 @@ public function __construct( array $meta = [], array $data = [], PoolInterface $pool = null, - ?AuthorizationInterface $auth = null + ?AuthorizationInterface $auth = null, + ?ArrayUtils $arrayUtils = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -197,6 +206,7 @@ public function __construct( $this->request = $request; $this->categoryFactory = $categoryFactory; $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->arrayUtils = $arrayUtils ?? ObjectManager::getInstance()->get(ArrayUtils::class); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); } @@ -226,7 +236,7 @@ public function getMeta() * @param array $meta * @return array */ - private function addUseDefaultValueCheckbox(Category $category, array $meta) + private function addUseDefaultValueCheckbox(Category $category, array $meta): array { /** @var EavAttributeInterface $attribute */ foreach ($category->getAttributes() as $attribute) { @@ -290,7 +300,7 @@ public function prepareMeta($meta) * @param array $fieldsMeta * @return array */ - private function prepareFieldsMeta($fieldsMap, $fieldsMeta) + private function prepareFieldsMeta(array $fieldsMap, array $fieldsMeta): array { $canEditDesign = $this->auth->isAllowed('Magento_Catalog::edit_category_design'); @@ -350,6 +360,8 @@ public function getAttributesMeta(Type $entityType) { $meta = []; $attributes = $entityType->getAttributeCollection(); + $fields = $this->getFields(); + $category = $this->getCurrentCategory(); /* @var EavAttribute $attribute */ foreach ($attributes as $attribute) { $code = $attribute->getAttributeCode(); @@ -374,6 +386,16 @@ public function getAttributesMeta(Type $entityType) $meta[$code]['scopeLabel'] = $this->getScopeLabel($attribute); $meta[$code]['componentType'] = Field::NAME; + + // disable fields + if ($category) { + $attributeIsLocked = $category->isLockedAttribute($code); + $meta[$code]['disabled'] = $attributeIsLocked; + $hasUseConfigField = (bool) array_search('use_config.' . $code, $fields, true); + if ($hasUseConfigField && $meta[$code]['disabled']) { + $meta['use_config.' . $code]['disabled'] = true; + } + } } $result = []; @@ -505,7 +527,7 @@ protected function filterFields($categoryData) * @param array $categoryData * @return array */ - private function convertValues($category, $categoryData) + private function convertValues($category, $categoryData): array { foreach ($category->getAttributes() as $attributeCode => $attribute) { if (!isset($categoryData[$attributeCode])) { @@ -616,13 +638,24 @@ protected function getFieldsMap() ]; } + /** + * Return list of fields names. + * + * @return array + */ + private function getFields(): array + { + $fieldsMap = $this->getFieldsMap(); + return $this->arrayUtils->flatten($fieldsMap); + } + /** * Retrieve scope overridden value * * @return ScopeOverriddenValue * @deprecated 101.1.0 */ - private function getScopeOverriddenValue() + private function getScopeOverriddenValue(): ScopeOverriddenValue { if (null === $this->scopeOverriddenValue) { $this->scopeOverriddenValue = \Magento\Framework\App\ObjectManager::getInstance()->get( @@ -639,7 +672,7 @@ private function getScopeOverriddenValue() * @return ArrayManager * @deprecated 101.1.0 */ - private function getArrayManager() + private function getArrayManager(): ArrayManager { if (null === $this->arrayManager) { $this->arrayManager = \Magento\Framework\App\ObjectManager::getInstance()->get( @@ -657,7 +690,7 @@ private function getArrayManager() * * @deprecated 101.1.0 */ - private function getFileInfo() + private function getFileInfo(): FileInfo { if ($this->fileInfo === null) { $this->fileInfo = ObjectManager::getInstance()->get(FileInfo::class); diff --git a/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php b/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php new file mode 100644 index 0000000000000..db1b84ed27772 --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategoryAttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Category Attribute search results. + */ +class CategoryAttributeSearchResults extends SearchResults implements CategoryAttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/CategorySearchResults.php b/app/code/Magento/Catalog/Model/CategorySearchResults.php new file mode 100644 index 0000000000000..7590ee4a23eda --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategorySearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\CategorySearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Category search results. + */ +class CategorySearchResults extends SearchResults implements CategorySearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index cb708695255d4..15ba6c8f3758b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -105,7 +105,7 @@ public function execute(array $entityIds = [], $useTempTable = false) * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface * @throws \DomainException */ - private function getProductIdsWithParents(array $childProductIds) + private function getProductIdsWithParents(array $childProductIds): array { /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); @@ -123,8 +123,12 @@ private function getProductIdsWithParents(array $childProductIds) ); $parentProductIds = $this->connection->fetchCol($select); + $ids = array_unique(array_merge($childProductIds, $parentProductIds)); + foreach ($ids as $key => $id) { + $ids[$key] = (int) $id; + } - return array_unique(array_merge($childProductIds, $parentProductIds)); + return $ids; } /** @@ -175,7 +179,7 @@ protected function removeEntries() protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getNonAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?) OR relation.child_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); } /** @@ -216,28 +220,28 @@ protected function isRangingNeeded() * Returns a list of category ids which are assigned to product ids in the index * * @param array $productIds - * @return \Magento\Framework\Indexer\CacheContext + * @return array */ - private function getCategoryIdsFromIndex(array $productIds) + private function getCategoryIdsFromIndex(array $productIds): array { $categoryIds = []; foreach ($this->storeManager->getStores() as $store) { - $categoryIds = array_merge( - $categoryIds, - $this->connection->fetchCol( - $this->connection->select() - ->from($this->getIndexTable($store->getId()), ['category_id']) - ->where('product_id IN (?)', $productIds) - ->distinct() - ) + $storeCategories = $this->connection->fetchCol( + $this->connection->select() + ->from($this->getIndexTable($store->getId()), ['category_id']) + ->where('product_id IN (?)', $productIds) + ->distinct() ); + $categoryIds[] = $storeCategories; } - $parentCategories = $categoryIds; + $categoryIds = array_merge(...$categoryIds); + + $parentCategories = [$categoryIds]; foreach ($categoryIds as $categoryId) { $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); - $parentCategories = array_merge($parentCategories, $parentIds); + $parentCategories[] = $parentIds; } - $categoryIds = array_unique($parentCategories); + $categoryIds = array_unique(array_merge(...$parentCategories)); return $categoryIds; } diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php index 9d7b71c981c6b..b8e9b8ad4aaa5 100644 --- a/app/code/Magento/Catalog/Model/Layer/FilterList.php +++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Layer; +/** + * Layer navigation filters + */ class FilterList { const CATEGORY_FILTER = 'category'; @@ -106,9 +110,9 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\ { $filterClassName = $this->filterTypes[self::ATTRIBUTE_FILTER]; - if ($attribute->getAttributeCode() == 'price') { + if ($attribute->getFrontendInput() === 'price') { $filterClassName = $this->filterTypes[self::PRICE_FILTER]; - } elseif ($attribute->getBackendType() == 'decimal') { + } elseif ($attribute->getBackendType() === 'decimal') { $filterClassName = $this->filterTypes[self::DECIMAL_FILTER]; } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index e06e85e90a2d8..b374b754d7de1 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\Operation\ExtensionInterface; use Magento\MediaStorage\Model\File\Uploader as FileUploader; +use Magento\Store\Model\StoreManagerInterface; /** * Create handler for catalog product gallery @@ -74,6 +79,16 @@ class CreateHandler implements ExtensionInterface */ private $mediaAttributeCodes; + /** + * @var array + */ + private $imagesGallery; + + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository @@ -82,6 +97,8 @@ class CreateHandler implements ExtensionInterface * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb + * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\EntityManager\MetadataPool $metadataPool, @@ -90,7 +107,8 @@ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb + \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, + \Magento\Store\Model\StoreManagerInterface $storeManager = null ) { $this->metadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $this->attributeRepository = $attributeRepository; @@ -99,6 +117,7 @@ public function __construct( $this->mediaConfig = $mediaConfig; $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fileStorageDb = $fileStorageDb; + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -137,6 +156,10 @@ public function execute($product, $arguments = []) if ($product->getIsDuplicate() != true) { foreach ($value['images'] as &$image) { + if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + $image['removed'] = ''; + } + if (!empty($image['removed'])) { $clearImages[] = $image['file']; } elseif (empty($image['value_id'])) { @@ -152,6 +175,10 @@ public function execute($product, $arguments = []) // For duplicating we need copy original images. $duplicate = []; foreach ($value['images'] as &$image) { + if (!empty($image['removed']) && !$this->canRemoveImage($product, $image['file'])) { + $image['removed'] = ''; + } + if (empty($image['value_id']) || !empty($image['removed'])) { continue; } @@ -538,4 +565,46 @@ private function processMediaAttributeLabel( ); } } + + /** + * Get product images for all stores + * + * @param ProductInterface $product + * @return array + */ + private function getImagesForAllStores(ProductInterface $product) + { + if ($this->imagesGallery === null) { + $storeIds = array_keys($this->storeManager->getStores()); + $storeIds[] = 0; + + $this->imagesGallery = $this->resourceModel->getProductImages($product, $storeIds); + } + + return $this->imagesGallery; + } + + /** + * Check possibility to remove image + * + * @param ProductInterface $product + * @param string $imageFile + * @return bool + */ + private function canRemoveImage(ProductInterface $product, string $imageFile) :bool + { + $canRemoveImage = true; + $gallery = $this->getImagesForAllStores($product); + $storeId = $product->getStoreId(); + + if (!empty($gallery)) { + foreach ($gallery as $image) { + if ($image['filepath'] === $imageFile && (int) $image['store_id'] !== $storeId) { + $canRemoveImage = false; + } + } + } + + return $canRemoveImage; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Hydrator.php b/app/code/Magento/Catalog/Model/Product/Hydrator.php new file mode 100644 index 0000000000000..dcdce7202b212 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Hydrator.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Framework\EntityManager\HydratorInterface; + +/** + * Class is used to extract data and populate entity with data + */ +class Hydrator implements HydratorInterface +{ + /** + * @inheritdoc + */ + public function extract($entity) + { + return $entity->getData(); + } + + /** + * @inheritdoc + */ + public function hydrate($entity, array $data) + { + $lockedAttributes = $entity->getLockedAttributes(); + $entity->unlockAttributes(); + $entity->setData(array_merge($entity->getData(), $data)); + foreach ($lockedAttributes as $attribute) { + $entity->lockAttribute($attribute); + } + + return $entity; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 2b4739ebeb736..6ac48c565e842 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -159,7 +159,7 @@ public function prepareForCart() if ($this->_dateExists()) { if ($this->useCalendar()) { - $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp(); + $timestamp += $this->_localeDate->date($value['date'], null, false, false)->getTimestamp(); } else { $timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); } diff --git a/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php b/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php new file mode 100644 index 0000000000000..776009089b9aa --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductAttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Product Attribute search results. + */ +class ProductAttributeSearchResults extends SearchResults implements ProductAttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/ProductSearchResults.php b/app/code/Magento/Catalog/Model/ProductSearchResults.php new file mode 100644 index 0000000000000..7aa3b4d961c23 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Product search results. + */ +class ProductSearchResults extends SearchResults implements ProductSearchResultsInterface +{ +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 786cec391c460..9e0d174a4cccb 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -9,13 +9,17 @@ * * @author Magento Core Team <core@magentocommerce.com> */ +declare(strict_types=1); + namespace Magento\Catalog\Model\ResourceModel; use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; -use Magento\Catalog\Model\Category as CategoryEntity; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Api\Data\ProductInterface; /** * Resource model for category entity @@ -92,6 +96,12 @@ class Category extends AbstractResource * @var Processor */ private $indexerProcessor; + + /** + * @var MetadataPool + */ + private $metadataPool; + /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -103,6 +113,7 @@ class Category extends AbstractResource * @param Processor $indexerProcessor * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param MetadataPool|null $metadataPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -114,7 +125,8 @@ public function __construct( \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, Processor $indexerProcessor, $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + MetadataPool $metadataPool = null ) { parent::__construct( $context, @@ -129,6 +141,7 @@ public function __construct( $this->indexerProcessor = $indexerProcessor; $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** @@ -275,7 +288,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $object) if ($object->getPosition() === null) { $object->setPosition($this->_getMaxPosition($object->getPath()) + 1); } - $path = explode('/', $object->getPath()); + $path = explode('/', (string)$object->getPath()); $level = count($path) - ($object->getId() ? 1 : 0); $toUpdateChild = array_diff($path, [$object->getId()]); @@ -314,7 +327,7 @@ protected function _afterSave(\Magento\Framework\DataObject $object) /** * Add identifier for new category */ - if (substr($object->getPath(), -1) == '/') { + if (substr((string)$object->getPath(), -1) == '/') { $object->setPath($object->getPath() . $object->getId()); $this->_savePath($object); } @@ -352,7 +365,7 @@ protected function _getMaxPosition($path) { $connection = $this->getConnection(); $positionField = $connection->quoteIdentifier('position'); - $level = count(explode('/', $path)); + $level = count(explode('/', (string)$path)); $bind = ['c_level' => $level, 'c_path' => $path . '/%']; $select = $connection->select()->from( $this->getTable('catalog_category_entity'), @@ -717,7 +730,7 @@ public function getCategories($parent, $recursionLevel = 0, $sorted = false, $as */ public function getParentCategories($category) { - $pathIds = array_reverse(explode(',', $category->getPathInStore())); + $pathIds = array_reverse(explode(',', (string)$category->getPathInStore())); /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */ $categories = $this->_categoryCollectionFactory->create(); return $categories->setStore( @@ -1134,4 +1147,45 @@ private function getAggregateCount() } return $this->aggregateCount; } + + /** + * Get category with children. + * + * @param int $categoryId + * @return array + */ + public function getCategoryWithChildren(int $categoryId): array + { + $connection = $this->getConnection(); + + $selectAttributeCode = $connection->select() + ->from( + ['eav_attribute' => $this->getTable('eav_attribute')], + ['attribute_id'] + )->where('entity_type_id = ?', CategorySetup::CATEGORY_ENTITY_TYPE_ID) + ->where('attribute_code = ?', 'is_anchor') + ->limit(1); + $isAnchorAttributeCode = $connection->fetchOne($selectAttributeCode); + if (empty($isAnchorAttributeCode) || (int)$isAnchorAttributeCode <= 0) { + return []; + } + + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $select = $connection->select() + ->from( + ['cce' => $this->getTable('catalog_category_entity')], + [$linkField, 'parent_id', 'path'] + )->join( + ['cce_int' => $this->getTable('catalog_category_entity_int')], + 'cce.' . $linkField . ' = cce_int.' . $linkField, + ['is_anchor' => 'cce_int.value'] + )->where( + 'cce_int.attribute_id = ?', + $isAnchorAttributeCode + )->where( + "cce.path LIKE '%/{$categoryId}' OR cce.path LIKE '%/{$categoryId}/%'" + )->order('path'); + + return $connection->fetchAll($select); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 23f612582f42e..355561c5e384d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -845,6 +845,9 @@ public function afterDelete() /** * @inheritdoc * @since 100.0.9 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -858,6 +861,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.9 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index dbd6a7a2e1094..e1350ebb25c87 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\ResourceModel\Product; @@ -22,6 +23,7 @@ use Magento\Framework\Indexer\DimensionFactory; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Category; /** * Product collection @@ -302,6 +304,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ private $urlFinder; + /** + * @var Category + */ + private $categoryResourceModel; + /** * Collection constructor * @@ -330,6 +337,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory + * @param Category|null $categoryResourceModel * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -358,7 +366,8 @@ public function __construct( MetadataPool $metadataPool = null, TableMaintainer $tableMaintainer = null, PriceTableResolver $priceTableResolver = null, - DimensionFactory $dimensionFactory = null + DimensionFactory $dimensionFactory = null, + Category $categoryResourceModel = null ) { $this->moduleManager = $moduleManager; $this->_catalogProductFlatState = $catalogProductFlatState; @@ -392,6 +401,8 @@ public function __construct( $this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class); $this->dimensionFactory = $dimensionFactory ?: ObjectManager::getInstance()->get(DimensionFactory::class); + $this->categoryResourceModel = $categoryResourceModel ?: ObjectManager::getInstance() + ->get(Category::class); } /** @@ -1584,6 +1595,8 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = } else { return parent::addAttributeToFilter($attribute, $condition, $joinType); } + + return $this; } /** @@ -1673,7 +1686,11 @@ public function addFilterByRequiredOptions() public function setVisibility($visibility) { $this->_productLimitationFilters['visibility'] = $visibility; - $this->_applyProductLimitations(); + if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { + $this->addAttributeToFilter('visibility', $visibility); + } else { + $this->_applyProductLimitations(); + } return $this; } @@ -2053,12 +2070,13 @@ protected function _applyProductLimitations() protected function _applyZeroStoreProductLimitations() { $filters = $this->_productLimitationFilters; + $categories = $this->getChildrenCategories((int)$filters['category_id']); $conditions = [ 'cat_pro.product_id=e.entity_id', $this->getConnection()->quoteInto( - 'cat_pro.category_id=?', - $filters['category_id'] + 'cat_pro.category_id IN (?)', + $categories ), ]; $joinCond = join(' AND ', $conditions); @@ -2079,6 +2097,40 @@ protected function _applyZeroStoreProductLimitations() return $this; } + /** + * Get children categories. + * + * @param int $categoryId + * @return array + */ + private function getChildrenCategories(int $categoryId): array + { + $categoryIds[] = $categoryId; + $anchorCategory = []; + + $categories = $this->categoryResourceModel->getCategoryWithChildren($categoryId); + if (empty($categories)) { + return $categoryIds; + } + + $firstCategory = array_shift($categories); + if ($firstCategory['is_anchor'] == 1) { + $linkField = $this->getProductEntityMetadata()->getLinkField(); + $anchorCategory[] = (int)$firstCategory[$linkField]; + foreach ($categories as $category) { + if (in_array($category['parent_id'], $categoryIds) + && in_array($category['parent_id'], $anchorCategory)) { + $categoryIds[] = (int)$category[$linkField]; + if ($category['is_anchor'] == 1) { + $anchorCategory[] = (int)$category[$linkField]; + } + } + } + } + + return $categoryIds; + } + /** * Add category ids to loaded items * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index 7730d7cc9a7fd..e625e38b59f31 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -8,6 +8,8 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; /** * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model @@ -199,13 +201,52 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'dd.attribute_id', 's.store_id', 'value' => new \Zend_Db_Expr('COALESCE(ds.value, dd.value)'), - 'cpe.entity_id', + 'cpe.entity_id AS source_id', ] ); if ($entityIds !== null) { $ids = implode(',', array_map('intval', $entityIds)); + $selectWithoutDefaultStore = $connection->select()->from( + ['wd' => $this->getTable('catalog_product_entity_int')], + [ + 'cpe.entity_id', + 'attribute_id', + 'store_id', + 'value', + 'cpe.entity_id', + ] + )->joinLeft( + ['cpe' => $this->getTable('catalog_product_entity')], + "cpe.{$productIdField} = wd.{$productIdField}", + [] + )->joinLeft( + ['d2d' => $this->getTable('catalog_product_entity_int')], + sprintf( + "d2d.store_id = 0 AND d2d.{$productIdField} = wd.{$productIdField} AND d2d.attribute_id = %s", + $this->_eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId() + ), + [] + )->joinLeft( + ['d2s' => $this->getTable('catalog_product_entity_int')], + "d2s.store_id != 0 AND d2s.attribute_id = d2d.attribute_id AND " . + "d2s.{$productIdField} = d2d.{$productIdField}", + [] + ) + ->where((new \Zend_Db_Expr('COALESCE(d2s.value, d2d.value)')) . ' = ' . ProductStatus::STATUS_ENABLED) + ->where("wd.attribute_id IN({$attrIdsFlat})") + ->where('wd.value IS NOT NULL') + ->where('wd.store_id != 0') + ->where("cpe.entity_id IN({$ids})"); $select->where("cpe.entity_id IN({$ids})"); + $selects = new UnionExpression( + [$select, $selectWithoutDefaultStore], + Select::SQL_UNION, + '( %s )' + ); + + $select = $connection->select(); + $select->from(['u' => $selects]); } /** @@ -342,7 +383,7 @@ private function getMultiSelectAttributeWithSourceModels($attrIds) ProductAttributeInterface::ENTITY_TYPE_CODE, $criteria )->getItems(); - + $options = []; foreach ($attributes as $attribute) { $sourceModelOptions = $attribute->getOptions(); diff --git a/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php b/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php new file mode 100644 index 0000000000000..751fa3fdfad84 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/FlushCategoryPagesCache.php @@ -0,0 +1,60 @@ +<?php declare(strict_types=1); +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Event\Observer as Event; +use Magento\Framework\Event\ObserverInterface; +use Magento\PageCache\Model\Cache\Type as PageCache; +use Magento\PageCache\Model\Config as CacheConfig; + +/** + * Flush the built in page cache when a category is moved + */ +class FlushCategoryPagesCache implements ObserverInterface +{ + + /** + * @var CacheConfig + */ + private $cacheConfig; + + /** + * + * @var PageCache + */ + private $pageCache; + + /** + * FlushCategoryPagesCache constructor. + * + * @param CacheConfig $cacheConfig + * @param PageCache $pageCache + */ + public function __construct(CacheConfig $cacheConfig, PageCache $pageCache) + { + $this->cacheConfig = $cacheConfig; + $this->pageCache = $pageCache; + } + + /** + * Clean the category page cache if built in cache page cache is used. + * + * The built in cache requires cleaning all pages that contain the top category navigation menu when a + * category is moved. This is because the built in cache does not support ESI blocks. + * + * @param Event $event + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute(Event $event) + { + if ($this->cacheConfig->getType() == CacheConfig::BUILT_IN && $this->cacheConfig->isEnabled()) { + $this->pageCache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [Category::CACHE_TAG]); + } + } +} diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php index 44f9193ab4012..b4aa5bd960b01 100644 --- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php +++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Plugin\Block; use Magento\Catalog\Model\Category; @@ -156,12 +158,13 @@ private function getCurrentCategory() */ private function getCategoryAsArray($category, $currentCategory, $isParentActive) { + $categoryId = $category->getId(); return [ 'name' => $category->getName(), - 'id' => 'category-node-' . $category->getId(), + 'id' => 'category-node-' . $categoryId, 'url' => $this->catalogCategory->getCategoryUrl($category), - 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true), - 'is_active' => $category->getId() == $currentCategory->getId(), + 'has_active' => in_array((string)$categoryId, explode('/', (string)$currentCategory->getPath()), true), + 'is_active' => $categoryId == $currentCategory->getId(), 'is_category' => true, 'is_parent_active' => $isParentActive ]; @@ -193,4 +196,22 @@ protected function getCategoryTree($storeId, $rootId) return $collection; } + + /** + * Add active + * + * @param \Magento\Theme\Block\Html\Topmenu $subject + * @param string[] $result + * @return string[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetCacheKeyInfo(\Magento\Theme\Block\Html\Topmenu $subject, array $result) + { + $activeCategory = $this->getCurrentCategory(); + if ($activeCategory) { + $result[] = Category::CACHE_TAG . '_' . $activeCategory->getId(); + } + + return $result; + } } diff --git a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index c39247f9b30df..0000000000000 --- a/app/code/Magento/Catalog/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tables = [ - 'catalog_product_index_price_cfg_opt_agr_tmp', - 'catalog_product_index_price_cfg_opt_tmp', - 'catalog_product_index_price_final_tmp', - 'catalog_product_index_price_opt_tmp', - 'catalog_product_index_price_opt_agr_tmp', - 'catalog_product_index_eav_tmp', - 'catalog_product_index_eav_decimal_tmp', - 'catalog_product_index_price_tmp', - 'catalog_category_product_index_tmp', - ]; - foreach ($tables as $table) { - $tableName = $this->schemaSetup->getTable($table); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml index c6492754515fb..e5cefda0aca96 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml @@ -15,8 +15,8 @@ <arguments> <argument name="image"/> </arguments> - - <conditionalClick selector="{{AdminProductImagesSection.productImagesToggleState('closed')}}" dependentSelector="{{AdminProductImagesSection.productImagesToggleState('open')}}" visible="false" stepKey="clickSectionImage"/> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageFile(image.fileName)}}" visible="false" stepKey="expandImages"/> + <waitForElementVisible selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="seeProductImageName"/> <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> <checkOption selector="{{AdminProductImagesSection.roleBase}}" stepKey="checkRoleBase"/> @@ -25,4 +25,14 @@ <checkOption selector="{{AdminProductImagesSection.roleSwatch}}" stepKey="checkRoleSwatch"/> <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> </actionGroup> + <actionGroup name="AdminAssignImageRolesIfUnassignedActionGroup" extends="AdminAssignImageRolesActionGroup"> + <annotations> + <description>Requires the navigation to the Product Creation page. Assign the Base, Small, Thumbnail, and Swatch Roles to image.</description> + </annotations> + + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Base')}}" visible="false" stepKey="checkRoleBase"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Small')}}" visible="false" stepKey="checkRoleSmall"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Thumbnail')}}" visible="false" stepKey="checkRoleThumbnail"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Swatch')}}" visible="false" stepKey="checkRoleSwatch"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index a114ea3edd563..afc332cc28378 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml @@ -99,7 +99,11 @@ <attachFile selector="{{AdminCategoryContentSection.uploadImageFile}}" userInput="{{image.file}}" stepKey="uploadFile"/> <waitForAjaxLoad time="30" stepKey="waitForAjaxUpload"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryContentSection.imageFileName}}" userInput="{{image.file}}" stepKey="seeImage"/> + <grabTextFrom selector="{{AdminCategoryContentSection.imageFileName}}" stepKey="grabCategoryFileName"/> + <assertRegExp stepKey="assertEquals" message="pass"> + <expectedResult type="string">/magento-logo(_[0-9]+)*?\.png$/</expectedResult> + <actualResult type="variable">grabCategoryFileName</actualResult> + </assertRegExp> </actionGroup> <!-- Remove image from category --> @@ -128,7 +132,11 @@ <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.uploadButton}}" visible="false" stepKey="openContentSection"/> <waitForPageLoad stepKey="waitForPageLoad"/> <waitForElementVisible selector="{{AdminCategoryContentSection.uploadButton}}" stepKey="seeImageSectionIsReady"/> - <see selector="{{AdminCategoryContentSection.imageFileName}}" userInput="{{image.file}}" stepKey="seeImage"/> + <grabTextFrom selector="{{AdminCategoryContentSection.imageFileName}}" stepKey="grabCategoryFileName"/> + <assertRegExp stepKey="assertEquals" message="pass"> + <expectedResult type="string">/magento-logo(_[0-9]+)*?\.png$/</expectedResult> + <actualResult type="variable">grabCategoryFileName</actualResult> + </assertRegExp> </actionGroup> <!-- Action to navigate to Media Gallery. Used in tests to cleanup uploaded images --> @@ -183,6 +191,18 @@ <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="dontSeeCategoryInTree"/> </actionGroup> + <actionGroup name="AdminDeleteCategoryByName" extends="DeleteCategory"> + <arguments> + <argument name="categoryName" type="string" defaultValue="category1"/> + </arguments> + <remove keyForRemoval="clickCategoryLink"/> + <remove keyForRemoval="dontSeeCategoryInTree"/> + <remove keyForRemoval="expandToSeeAllCategories"/> + <conditionalClick selector="{{AdminCategorySidebarTreeSection.expandAll}}" dependentSelector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" visible="false" stepKey="expandCategories" after="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" stepKey="clickCategory" after="expandCategories"/> + <conditionalClick selector="{{AdminCategorySidebarTreeSection.expandAll}}" dependentSelector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" visible="false" stepKey="expandCategoriesToSeeAll" after="seeDeleteSuccess"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryByName(categoryName)}}" stepKey="dontSeeCategory" after="expandCategoriesToSeeAll"/> + </actionGroup> <!-- Actions to fill out a new category from the product page--> <!-- The action assumes that you are already on an admin product configuration page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 5c5ee0f9cb321..428b3828901cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -24,7 +24,7 @@ <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> </actionGroup> - + <!--Navigate to create product page directly via ID--> <actionGroup name="goToProductPageViaID"> <annotations> @@ -108,6 +108,12 @@ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> </actionGroup> + <actionGroup name="AdminFillProductCountryOfManufactureActionGroup"> + <arguments> + <argument name="countryId" type="string" defaultValue="US"/> + </arguments> + <selectOption selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="{{countryId}}" stepKey="countryOfManufactureDropDown"/> + </actionGroup> <!--Check that required fields are actually required--> <actionGroup name="checkRequiredFieldsInProductForm"> @@ -184,6 +190,18 @@ <click selector="{{AdminProductImagesSection.removeImageButton}}" stepKey="clickRemoveImage"/> </actionGroup> + <!--Remove Product image by name--> + <actionGroup name="RemoveProductImageByName" extends="removeProductImage"> + <annotations> + <description>Removes a Product Image on the Admin Products creation/edit page by name.</description> + </annotations> + + <arguments> + <argument name="image" defaultValue="ProductImage"/> + </arguments> + <click selector="{{AdminProductImagesSection.removeImageButtonForExactImage(image.fileName)}}" stepKey="clickRemoveImage"/> + </actionGroup> + <!-- Assert product image in Admin Product page --> <actionGroup name="assertProductImageAdminProductPage"> <annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index aef79e651b584..320a322fc5f8e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -389,7 +389,7 @@ <waitForPageLoad stepKey="waitForGridLoad"/> </actionGroup> - <!--Filter and select the the product --> + <!--Filter and select the product --> <actionGroup name="filterAndSelectProduct"> <annotations> <description>Goes to the Admin Products grid. Filters the Product grid by the provided Product SKU.</description> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml new file mode 100644 index 0000000000000..a416957dabc2b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DisableProductLabelActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DisableProductLabelActionGroup"> + <annotations> + <description>Disable Product Label and Change Attribute Set.</description> + </annotations> + <arguments> + <argument name="createAttributeSet"/> + </arguments> + + <checkOption selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad time="30" stepKey="waitForChangeAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttrSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{createAttributeSet.attribute_set_name}}" stepKey="searchForAttrSet"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="dontSeeCheckboxEnableProductIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..4c641b621a504 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductAbsentOnCategoryPageActionGroup"> + <annotations> + <description>Navigate to category page and verify product is absent.</description> + </annotations> + <arguments> + <argument name="category" defaultValue="_defaultCategory"/> + <argument name="product" defaultValue="SimpleProduct"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url(category.name)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{product.name}}" stepKey="assertProductIsNotPresent"/> + <dontSee selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="{{product.price}}" stepKey="assertProductIsNotPricePresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml index 899603aa27d75..e0229906ad558 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml @@ -18,4 +18,15 @@ <amOnPage url="{{StorefrontProductPage.url(productUrl)}}" stepKey="openProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoaded"/> </actionGroup> + <actionGroup name="StorefrontOpenProductPageOnSecondStore"> + <annotations> + <description>Goes to the Storefront Product page for the provided store code and Product URL.</description> + </annotations> + <arguments> + <argument name="storeCode" type="string"/> + <argument name="productUrl" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontStoreViewProductPage.url(storeCode,productUrl)}}" stepKey="openProductPage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml new file mode 100644 index 0000000000000..cb2bacfd2f2da --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogConfigurationData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!-- Catalog > Price --> + <entity name="GlobalCatalogPriceScopeConfigData"> + <!-- Default configuration --> + <data key="path">catalog/price/scope</data> + <data key="scope_id">0</data> + <data key="label">Global</data> + <data key="value">0</data> + </entity> + <entity name="WebsiteCatalogPriceScopeConfigData"> + <data key="path">catalog/price/scope</data> + <data key="scope_id">0</data> + <data key="label">Website</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml new file mode 100644 index 0000000000000..50ce7f2da18c7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceConfigData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogPriceScopeWebsiteConfigData"> + <data key="path">catalog/price/scope</data> + <data key="value">1</data> + </entity> + <entity name="CatalogPriceScopeGlobalConfigData"> + <data key="path">catalog/price/scope</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml index 7bd392f0aa74a..1effb4ed0664e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml @@ -55,4 +55,20 @@ <data key="attribute_code">is_anchor</data> <data key="value">0</data> </entity> + <entity name="ProductDescriptionAdvancedSearchABC" type="custom_attribute"> + <data key="attribute_code">description</data> + <data key="value"><p>adc_Full</p></data> + </entity> + <entity name="ProductShortDescriptionAdvancedSearch" type="custom_attribute"> + <data key="attribute_code">short_description</data> + <data key="value"><p>abc_short</p></data> + </entity> + <entity name="ProductDescriptionAdvancedSearchADC123" type="custom_attribute"> + <data key="attribute_code">description</data> + <data key="value"><p>dfj_full</p></data> + </entity> + <entity name="ProductShortDescriptionAdvancedSearchADC123" type="custom_attribute"> + <data key="attribute_code">short_description</data> + <data key="value"><p>dfj_short</p></data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 1986821f899cf..6614fa4b5dbeb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -278,6 +278,28 @@ <data key="used_for_sort_by">false</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeTypeOfPrice" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">price</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_wysiwyg_enabled">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> <entity name="textProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> <data key="frontend_input">text</data> <data key="default_value" unique="suffix">defaultValue</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index e122615eb8aa4..aad43bb7011c1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -438,6 +438,34 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiProductNameWithNoSpaces" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">ApiSimpleProduct</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> + <entity name="ApiProductWithDescriptionAndUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_simple_product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">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="_newDefaultProduct" type="product"> <data key="sku" unique="suffix">testSku</data> <data key="type_id">simple</data> @@ -531,6 +559,20 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiVirtualProductWithDescriptionAndUnderscoredSku" type="product"> + <data key="sku" unique="suffix">api_virtual_product</data> + <data key="type_id">virtual</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Virtual Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-virtual-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="SimpleProductWithNewFromDate" type="product"> <data key="sku" unique="suffix">SimpleProduct</data> <data key="type_id">simple</data> @@ -1224,4 +1266,41 @@ <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ABC_dfj_SimpleProduct" type="product"> + <data key="name" unique="suffix">abc_dfj_</data> + <data key="sku" unique="suffix">abc_dfj</data> + <data key="price">50.00</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductDescriptionAdvancedSearchABC</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductShortDescriptionAdvancedSearch</requiredEntity> + </entity> + <entity name="ABC_123_SimpleProduct" type="product"> + <data key="name" unique="suffix">adc_123_</data> + <data key="sku" unique="suffix">adc_123</data> + <data key="price">100.00</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductDescriptionAdvancedSearchADC123</requiredEntity> + <requiredEntity type="custom_attribute_array">ProductShortDescriptionAdvancedSearchADC123</requiredEntity> + </entity> + <entity name="SimpleProductUpdatePrice11" type="product2"> + <data key="price">11.00</data> + </entity> + <entity name="SimpleProductUpdatePrice14" type="product2"> + <data key="price">14.00</data> + </entity> + <entity name="SimpleProductUpdatePrice16" type="product2"> + <data key="price">16.00</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml new file mode 100644 index 0000000000000..933276edd834c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductFormData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductFormMessages" type="message"> + <data key="remove_image_notice">The image cannot be removed as it has been assigned to the other image role</data> + <data key="save_success">You saved the product.</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml new file mode 100644 index 0000000000000..046323bb368da --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontStoreViewProductPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <!-- It is created to open product page with store code setting--> + <page name="StorefrontStoreViewProductPage" url="/{{storeCode}}/{{productUrlKey}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml index e8adede5b2de6..4aca4a09602b3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml @@ -13,6 +13,7 @@ <element name="DeleteButton" type="button" selector=".page-actions-inner #delete" timeout="30"/> <element name="CategoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> <element name="CategoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> + <element name="CategoryStoreViewOptionSelected" type="button" selector="//div[contains(@class, 'store-switcher')]//div[contains(@class,'actions')]//button[contains(text(),'{{store}}')]" parameterized="true"/> <element name="CategoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> <element name="storeSwitcher" type="text" selector=".store-switcher"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml index e9ff40f98bb16..8a993a74a58d1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsSection.xml @@ -11,5 +11,6 @@ <section name="AdminCategoryProductsSection"> <element name="sectionHeader" type="button" selector="div[data-index='assign_products']" timeout="30"/> <element name="addProducts" type="button" selector="#catalog_category_add_product_tabs" timeout="30"/> + <element name="addProductsDisabled" type="button" selector="#catalog_category_add_product_tabs[disabled]" timeout="30"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index fba28b3feaff1..af7e2786f6e8d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -11,11 +11,14 @@ <section name="AdminCategorySidebarTreeSection"> <element name="collapseAll" type="button" selector=".tree-actions a:first-child"/> <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> + <element name="categoryHighlighted" type="text" selector="//div[@id='store.menu']//span[contains(text(),'{{name}}')]/ancestor::li" parameterized="true" timeout="30"/> + <element name="categoryNotHighlighted" type="text" selector="ul[id=\'ui-id-2\'] li[class~=\'active\']" timeout="30"/> <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> <element name="categoryInTreeUnderRoot" type="text" selector="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> <element name="lastCreatedCategory" type="block" selector=".x-tree-root-ct li li:last-child" /> <element name="treeContainer" type="block" selector=".tree-holder" /> <element name="expandRootCategory" type="text" selector="img.x-tree-elbow-end-plus"/> + <element name="categoryByName" type="text" selector="//div[contains(@class, 'categories-side-col')]//a/span[contains(text(), '{{categoryName}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 80b4159167453..7388ebc8408dd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -8,6 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormSection"> + <element name="datepickerNewAttribute" type="input" selector="[data-index='{{attrName}}'] input" timeout="30" parameterized="true"/> <element name="attributeSet" type="select" selector="div[data-index='attribute_set_id'] .admin__field-control"/> <element name="attributeSetFilter" type="input" selector="div[data-index='attribute_set_id'] .admin__field-control input" timeout="30"/> <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> @@ -215,6 +216,7 @@ <element name="textAttributeByName" type="text" selector="//div[@data-index='attributes']//fieldset[contains(@class, 'admin__field') and .//*[contains(.,'{{var}}')]]//input" parameterized="true"/> <element name="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> <element name="attributeSection" type="block" selector="//div[@data-index='attributes']/div[contains(@class, 'admin__collapsible-content _show')]" timeout="30"/> + <element name="customAttribute" type="text" selector="product[{{attributecode}}]" timeout="30" parameterized="true"/> <element name="attributeGroupByName" type="button" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']" parameterized="true"/> <element name="attributeByGroupAndName" type="text" selector="//div[@class='fieldset-wrapper-title']//span[text()='{{group}}']/../../following-sibling::div//span[contains(text(),'attribute')]" parameterized="true"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml index 89eb1ed678cc9..f20e9b3a11e5e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml @@ -14,6 +14,7 @@ <element name="imageUploadButton" type="button" selector="div.image div.fileinput-button"/> <element name="imageFile" type="text" selector="//*[@id='media_gallery_content']//img[contains(@src, '{{url}}')]" parameterized="true"/> <element name="removeImageButton" type="button" selector=".action-remove"/> + <element name="removeImageButtonForExactImage" type="button" selector="[id='media_gallery_content'] img[src*='{{imageName}}'] + div[class='actions'] button[class='action-remove']" parameterized="true"/> <element name="modalOkBtn" type="button" selector="button.action-primary.action-accept"/> <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> @@ -27,6 +28,7 @@ <element name="roleSmall" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Small']"/> <element name="roleThumbnail" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Thumbnail']"/> <element name="roleSwatch" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Swatch']"/> + <element name="isRoleChecked" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = '{{role}}']/parent::li[contains(@class,'selected')]" parameterized="true"/> <element name="isBaseSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Base']"/> <element name="isSmallSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Small']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml index ddec4428f90e2..faee605e77319 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontCategoryFilterSection"> <element name="CategoryFilter" type="button" selector="//main//div[@class='filter-options']//div[contains(text(), 'Category')]"/> <element name="CategoryByName" type="button" selector="//main//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{var1}}')]" parameterized="true"/> + <element name="CustomPriceAttribute" type="button" selector="div.filter-options-title"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml index 1b7bbd58eea9f..136a8ceadb89e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -12,6 +12,8 @@ <element name="filterOptions" type="text" selector=".filter-options-content .items"/> <element name="filterOption" type="text" selector=".filter-options-content .item"/> <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> + <element name="filterOptionByLabel" type="button" selector=" div.filter-options-item div[option-label='{{optionLabel}}']" parameterized="true"/> + <element name="removeFilter" type="button" selector="div.filter-current .remove"/> </section> <section name="StorefrontCategorySidebarMobileSection"> <element name="shopByButton" type="button" selector="//div[contains(@class, 'filter-title')]/strong[contains(text(), 'Shop By')]"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index feb4fffd12f5d..51ef7fb77d74c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -60,6 +60,9 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index e1cb45be22b4e..a863de2716c97 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -63,6 +63,10 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml index 15171fe3713c3..784b5d3fd1827 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml @@ -19,7 +19,7 @@ <group value="category"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 9115004ad9585..a6890c2ad4905 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -64,6 +64,9 @@ <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Verify the Category Title--> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!--Verify Product in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 5c798db29b976..63a964f4b5e91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -90,6 +90,9 @@ <waitForPageLoad stepKey="waitForProductToSave"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Run Re-Index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify product attribute added in product form --> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml index 6658ad36d7150..291b6985bd3e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml @@ -23,7 +23,7 @@ </createData> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml index 6096ee1fa3996..a7587a5ed31fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml @@ -22,7 +22,6 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 896a28d0298e6..94d488f216b49 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 58737dd509743..23f772a395a7d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -117,6 +117,8 @@ <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml index 78247f4943596..9055e961f889f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -24,6 +24,9 @@ <createData entity="Simple_US_CA_Customer" stepKey="customer" /> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="virtualProductGeneralGroup"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml new file mode 100644 index 0000000000000..f334cbc218b7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteProductsImageInCaseOfMultipleStoresTest"> + <annotations> + <stories value="MultipleStores"/> + <features value="Catalog"/> + <title value="Delete products image in case of multiple stores"/> + <description value="Delete products image in case of multiple stores"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11466"/> + <useCaseId value="MC-15391"/> + <group value="Catalog"/> + </annotations> + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create new website, store and store view--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <!--Create Product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <createData entity="SubCategory" stepKey="createSubCategory"/> + <createData entity="NewRootCategory" stepKey="createRootCategory"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad0"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="['Default Category', $$createRootCategory.name$$, $$createSubCategory.name$$]" stepKey="fillCategory"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Add images to the product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <actionGroup ref="addProductImage" stepKey="addImageToProduct"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="addProductImage" stepKey="addImage1ToProduct"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + <!--Enable config to view created store view on store front--> + <createData entity="EnableWebUrlOptionsConfig" stepKey="enableWebUrlOptionsConfig"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="createRootCategory" stepKey="deleteRootCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="DefaultWebUrlOptionsConfig" stepKey="defaultWebUrlOptionsConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Grab new store view code--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToNewWebsitePage"/> + <waitForPageLoad stepKey="waitForStoresPageLoad"/> + <fillField userInput="{{NewWebSiteData.name}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickFirstRow"/> + <grabValueFrom selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="grabStoreViewCode"/> + <click selector="{{AdminNewStoreViewActionsSection.backButton}}" stepKey="clickBack"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickResetButton"/> + <waitForPageLoad stepKey="waitForStorePageLoad"/> + <!--Open product page on admin--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <!--Enable the newly created website and save the product--> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectWebsiteInProduct2"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> + <!--Reindex and flush cache--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Switch to 'Default Store View' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchDefaultStoreView"> + <argument name="storeViewName" value="'Default Store View'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad3"/> + <!--Assign all roles to first image on default store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct3"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad4"/> + <!--Assign all roles to first image on new store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage2"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct4"/> + <!--Switch to 'All Store Views' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchAllStoreView"> + <argument name="storeViewName" value="'All Store Views'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad5"/> + <!--Remove product image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct5"/> + <!--Assert notification and success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification"/> + <!--Reopen image tab and see the image is not deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab"/> + <waitForPageLoad stepKey="waitForImagesLoad"/> + <seeElement selector="{{AdminProductImagesSection.imageFile(ProductImage.fileName)}}" stepKey="seeImageIsNotDeleted"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView2"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad6"/> + <!--Assign all roles to second image on default store view--> + <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToSecondImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct6"/> + <!--Switch to 'All Store Views' scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchAllStoreView2"> + <argument name="storeViewName" value="'All Store Views'"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad7"/> + <!--Remove product image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductFirstImage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct7"/> + <!--Assert notification and success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage2"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification2"/> + <!--Reopen image tab and see the image is not deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab2"/> + <waitForPageLoad stepKey="waitForImagesLoad2"/> + <seeElement selector="{{AdminProductImagesSection.imageFile(ProductImage.fileName)}}" stepKey="seeImageIsNotDeleted2"/> + <!--Switch to newly created Store View scope and open product page--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="SwitchNewStoreView3"> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad8"/> + <!--Remove second image and save--> + <actionGroup ref="RemoveProductImageByName" stepKey="removeProductSecondImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct8"/> + <!--Assert success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage3"/> + <!--Reopen image tab and see the image is deleted--> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab3"/> + <waitForPageLoad stepKey="waitForImagesLoad3"/> + <dontSeeElement selector="{{AdminProductImagesSection.imageFile(TestImageNew.fileName)}}" stepKey="seeImageIsDeleted"/> + <!--Open Storefront on Default store view and assert image existence--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad0"/> + <grabAttributeFrom userInput="src" selector="{{StorefrontCategoryMainSection.mediaDescription($$createProduct.name$$)}}" stepKey="grabAttributeFromImage"/> + <assertContains expectedType="string" expected="{{ProductImage.filename}}" actual="$grabAttributeFromImage" stepKey="assertProductImageAbsence"/> + <!--Open Storefront on newly created store view and assert image absence--> + <amOnPage url="$grabStoreViewCode" stepKey="navigateToHomePageOfSpecificStore"/> + <waitForPageLoad stepKey="waitForHomePageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createSubCategory.name$$)}}" stepKey="clickCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <grabAttributeFrom userInput="src" selector="{{StorefrontCategoryMainSection.mediaDescription($$createProduct.name$$)}}" stepKey="grabAttributeFromImage2"/> + <assertContains expectedType="string" expected="small_image" actual="$grabAttributeFromImage2" stepKey="assertProductImageAbsence2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml new file mode 100644 index 0000000000000..dab1704d50bf3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDisableProductOnChangingAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Disabled product is enabled when change attribute set"/> + <title value="Verify product status while changing attribute set"/> + <description value="Value set for enabled product has to be shown when attribute set is changed"/> + <severity value="MAJOR"/> + <testCaseId value="MC-19716"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$$createAttributeSet.attribute_set_id$$/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="DisableProductLabelActionGroup" stepKey="disableWhileChangingAttributeSet" > + <argument name="createAttributeSet" value="$$createAttributeSet$$"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index bee13bec370da..18d4b9e341cc6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -18,6 +18,85 @@ <testCaseId value="MC-128"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> + <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> + <createData entity="ApiProductWithDescription" stepKey="createProductTwo"/> + <createData entity="ApiProductNameWithNoSpaces" stepKey="createProductThree"/> + </before> + <after> + <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="createProductThree" stepKey="deleteProductThree"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <!-- Search and select products --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + <seeInCurrentUrl stepKey="seeInUrl" url="catalog/product_action_attribute/edit/"/> + <!-- Switch store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewActionGroup"/> + <!-- Update attribute --> + <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> + <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> + <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + + <!-- Assert on storefront default view with partial word of product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> + <argument name="name" value="$$createProductOne.name$$"/> + <argument name="description" value="$$createProductOne.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <see userInput="2 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> + + <!-- Assert on storefront custom view with partial word of product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupCustom"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="StorefrontSwitchStoreViewActionGroup"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameCustom"> + <argument name="name" value="$$createProductOne.name$$"/> + <argument name="description" value="Updated $$createProductOne.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultCustom"/> + <see userInput="2 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInCustom"/> + + <!-- Assert Storefront default view with exact product name --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault1"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault1"> + <argument name="name" value="$$createProductThree.name$$"/> + <argument name="description" value="$$createProductThree.custom_attributes[description]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault1"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault1"/> + </test> + <test name="AdminMassUpdateProductAttributesStoreViewScopeMysqlTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product attributes"/> + <title value="Admin should be able to mass update product attributes in store view scope using the Mysql search engine"/> + <description value="Admin should be able to mass update product attributes in store view scope using the Mysql search engine"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-20467"/> + <group value="Catalog"/> + <group value="Product Attributes"/> + <group value="SearchEngineMysql"/> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml index e9b54e3f1a3dc..02e8157282dee 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -18,6 +18,157 @@ <testCaseId value="MAGETWO-59361"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + + <!--Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="Second Website"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!--Create Store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="Second Website"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + + <!--Create Store view --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> + <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> + <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForPageLoad stepKey="waitForPageLoad2" time="180" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> + <see userInput="You saved the store view." stepKey="seeSavedMessage" /> + + <!--Create a Simple Product 1 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct1"> + <argument name="product" value="simpleProductForMassUpdate"/> + <argument name="website" value="Second Website"/> + </actionGroup> + + <!--Create a Simple Product 2 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct2"> + <argument name="product" value="simpleProductForMassUpdate2"/> + <argument name="website" value="Second Website"/> + </actionGroup> + </before> + <after> + <!--Delete website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + + <!--Delete Products --> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> + <argument name="productName" value="simpleProductForMassUpdate.name"/> + </actionGroup> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> + <argument name="productName" value="simpleProductForMassUpdate2.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> + </after> + + <!-- Search and select products --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> + </actionGroup> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + + <!-- Filter to Second Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView" > + <argument name="customStore" value="'Second Store View'" /> + </actionGroup> + + <!-- Select Product 2 --> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOption"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabled"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabled"/> + + <!-- Filter to Default Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterDefaultStoreView"> + <argument name="customStore" value="'Default'" /> + </actionGroup> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct2IsEnabled"/> + + <!-- Assert on storefront default view with first product --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> + <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> + + <!-- Assert on storefront default view with second product --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefaultToSearchSecondProduct"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefaultWithSecondProduct"> + <argument name="name" value="{{simpleProductForMassUpdate2.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefaultForSecondProduct"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefaultSecondProductResults"/> + + <!--Enable the product in Default store view--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdownDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOptionDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabledDefaultStoreView"/> + <waitForPageLoad stepKey="waitForBulkUpdatePageDefaultStoreView"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabledDefaultStoreView"/> + + <!-- Assert on storefront default view --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault2"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault2"> + <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> + <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> + </test> + <test name="AdminMassUpdateProductStatusStoreViewScopeMysqlTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product status"/> + <title value="Admin should be able to mass update product statuses in store view scope using the Mysql search engine"/> + <description value="Admin should be able to mass update product statuses in store view scope using the Mysql search engine"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-20471"/> + <group value="Catalog"/> + <group value="Product Attributes"/> + <group value="SearchEngineMysql"/> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> @@ -147,5 +298,5 @@ </actionGroup> <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> - </test> -</tests> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index d17078d794b42..b613068893b0e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -59,6 +59,9 @@ <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index 264615ff6736f..f7fd81f28199f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..a11646cc46875 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVirtualProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product type switching"/> + <title value="Virtual product type switching on editing to Downloadable product"/> + <description value="Virtual product type switching on editing to Downloadable product"/> + <testCaseId value="MC-17954"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="VirtualProduct" stepKey="createProduct"/> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Change product type to Downloadable--> + <comment userInput="Change product type to Downloadable" stepKey="commentCreateDownloadable"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOptionPurchaseSeparately"/> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm"/> + <!--Assert downloadable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertDownloadableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> + <!--Assert downloadable product on storefront--> + <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontDownloadableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertDownloadableProductInStock"/> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinksInStorefront"/> + <seeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeDownloadableLink" /> + </test> + <test name="AdminDownloadableProductTypeSwitchingToSimpleProductTest" extends="AdminVirtualProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product type switching"/> + <title value="Downloadable product type switching on editing to Simple product"/> + <description value="Downloadable product type switching on editing to Simple product"/> + <testCaseId value="MC-17955"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!--Change product type to Simple--> + <comment userInput="Change product type to Simple Product" stepKey="commentCreateSimple"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + <!--Assert simple product on Admin product page grid--> + <comment userInput="Assert simple product in Admin product page grid" stepKey="commentAssertProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogSimpleProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterSimpleProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeSimpleProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeSimpleProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearSimpleProductFilters"/> + <!--Assert simple product on storefront--> + <comment userInput="Assert simple product on storefront" stepKey="commentAssertSimpleProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openSimpleProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontSimpleProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertSimpleProductInStock"/> + <dontSeeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="dontSeeDownloadableLink" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 240a5492355cf..ebae27a1f7182 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -18,7 +18,7 @@ <group value="Catalog"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml index 1cd0e15780c11..df50edd20410a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest.xml @@ -34,7 +34,7 @@ <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the first product edit page --> @@ -144,6 +144,8 @@ <!-- Save the second product --> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <!-- Go to the admin grid and see the uploaded image --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> @@ -186,7 +188,7 @@ <after> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the product edit page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 2ff83afa15e5e..0f63a72844452 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> <argument name="categoryEntity" value="_defaultCategory"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create category, change store view to default --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml index 1cb01ac11cb8f..ad110ceee32d2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -64,6 +64,10 @@ <!--Verify Category Title--> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Verify Category in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 637ae790c16c8..82395e5d6e0eb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -94,6 +94,9 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 045b3f3420ff6..4817b3497c97e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -94,6 +94,9 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> + <!--Run re-index task --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index 8ac56d09e5b42..9fa0e155a4fe7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductRegularPrice"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml index d28e9ddbb1271..e0e8360850983 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductTierPriceInStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml index 22dd2b0054db4..677cc4c65ce88 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualProductWithTierPriceInStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 29c7536d21621..f0148f3d384c1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -28,6 +28,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProduct"> + <argument name="product" value="updateVirtualTierPriceOutOfStock"/> + </actionGroup> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="logout" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml index a4c8b492d9d84..867f097042a17 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml @@ -36,7 +36,7 @@ <group value="Catalog"/> </annotations> <before> - <createData entity="ApiProductWithDescription" stepKey="product"/> + <createData entity="ApiProductWithDescriptionAndUnderscoredSku" stepKey="product"/> </before> <after> <deleteData createDataKey="product" stepKey="delete"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml index 84c3f81ef6dbf..07b802637b2e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml @@ -33,7 +33,7 @@ <group value="Catalog"/> </annotations> <before> - <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> + <createData entity="ApiVirtualProductWithDescriptionAndUnderscoredSku" stepKey="product"/> </before> </test> <test name="AdvanceCatalogSearchVirtualProductByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index 5cae81b36a323..674d46b9c18b1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -36,7 +36,7 @@ <createData entity="NewRootCategory" stepKey="createNewRootCategoryA"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 7c0de6da18caf..ad66214e902fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -17,6 +17,203 @@ <description value="User browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out."/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-87435"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createSimpleProduct1Image"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createSimpleProduct1Image1"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createSimpleProduct1"/> + + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createSimpleProduct2Image"> + <requiredEntity createDataKey="createSimpleProduct2"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct2" createDataKey="createSimpleProduct2"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct1Image" stepKey="deleteSimpleProduct1Image"/>--> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct1Image1" stepKey="deleteSimpleProduct1Image1"/>--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createSimpleProduct2Image" stepKey="deleteSimpleProduct2Image"/>--> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + + <!--Re-index--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!-- Step 1: User browses catalog --> + <comment userInput="Start of browsing catalog" stepKey="startOfBrowsingCatalog" /> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <waitForPageLoad stepKey="homeWaitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeWaitForWelcomeMessage"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome"/> + + <!-- Open Category --> + <comment userInput="Open category" stepKey="commentOpenCategory" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="browseClickCategory"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="browseAssertCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <!-- Check simple product 1 in category --> + <comment userInput="Check simple product 1 in category" stepKey="commentCheckSimpleProductInCategory" /> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="browseGrabSimpleProduct1ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabSimpleProduct1ImageSrc" stepKey="browseAssertSimpleProduct1ImageNotDefault"/> + <!-- Check simple product 2 in category --> + <comment userInput="Check simple product 2 in category" stepKey="commentCheckSimpleProduct2InCategory" /> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="browseAssertCategoryProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="browseGrabSimpleProduct2ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabSimpleProduct2ImageSrc" stepKey="browseAssertSimpleProduct2ImageNotDefault"/> + + <!-- View Simple Product 1 --> + <comment userInput="View simple product 1" stepKey="commentViewSimpleProduct1" after="browseAssertSimpleProduct2ImageNotDefault"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="browseClickCategorySimpleProduct1View" after="commentViewSimpleProduct1"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct1Viewloaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct1Page"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabSimpleProduct1PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabSimpleProduct1PageImageSrc" stepKey="browseAssertSimpleProduct1PageImageNotDefault"/> + + <!-- View Simple Product 2 --> + <comment userInput="View simple product 2" stepKey="commentViewSimpleProduct2" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickCategory1"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct2.name$$)}}" stepKey="browseClickCategorySimpleProduct2View"/> + <waitForLoadingMaskToDisappear stepKey="waitForSimpleProduct2ViewLoaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="browseAssertProduct2Page"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabSimpleProduct2PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabSimpleProduct2PageImageSrc" stepKey="browseAssertSimpleProduct2PageImageNotDefault"/> + <comment userInput="End of browsing catalog" stepKey="endOfBrowsingCatalog" after="browseAssertSimpleProduct2PageImageNotDefault"/> + + <!-- Step 4: User compares products --> + <comment userInput="Start of comparing products" stepKey="startOfComparingProducts" after="endOfBrowsingCatalog"/> + <!-- Add Simple Product 1 to comparison --> + <comment userInput="Add simple product 1 to comparison" stepKey="commentAddSimpleProduct1ToComparison" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategory" /> + <waitForLoadingMaskToDisappear stepKey="waitForCategoryloaded" /> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="compareAssertSimpleProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct1ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct1ImageSrc" stepKey="compareAssertSimpleProduct1ImageNotDefault"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="compareClickSimpleProduct1"/> + <waitForLoadingMaskToDisappear stepKey="waitForCompareSimpleProduct1loaded" /> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="compareAssertProduct1Page"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="compareGrabSimpleProduct1PageImageSrc"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$compareGrabSimpleProduct1PageImageSrc" stepKey="compareAssertSimpleProduct2PageImageNotDefault"/> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="compareAddSimpleProduct1ToCompare"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + + <!-- Add Simple Product 2 to comparison --> + <comment userInput="Add simple product 2 to comparison" stepKey="commentAddSimpleProduct2ToComparison" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategory1"/> + <waitForLoadingMaskToDisappear stepKey="waitForCompareCategory1loaded" /> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory1"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="compareAssertSimpleProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct2ImageSrc"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct2ImageSrc" stepKey="compareAssertSimpleProduct2ImageNotDefault"/> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="compareAddSimpleProduct2ToCompare"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + + <!-- Check products in comparison sidebar --> + <!-- Check simple product 1 in comparison sidebar --> + <comment userInput="Check simple product 1 in comparison sidebar" stepKey="commentCheckSimpleProduct1InComparisonSidebar" after="compareAddSimpleProduct2ToCompare"/> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct1InSidebar" after="commentCheckSimpleProduct1InComparisonSidebar"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- Check simple product 2 in comparison sidebar --> + <comment userInput="Check simple product 2 in comparison sidebar" stepKey="commentCheckSimpleProduct2InComparisonSidebar" /> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareSimpleProduct2InSidebar"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + + <!-- Check products on comparison page --> + <!-- Check simple product 1 on comparison page --> + <comment userInput="Check simple product 1 on comparison page" stepKey="commentCheckSimpleProduct1OnComparisonPage" after="compareSimpleProduct2InSidebar"/> + <actionGroup ref="StorefrontOpenAndCheckComparisionActionGroup" stepKey="compareOpenComparePage" after="commentCheckSimpleProduct1OnComparisonPage"/> + <actionGroup ref="StorefrontCheckCompareSimpleProductActionGroup" stepKey="compareAssertSimpleProduct1InComparison"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct1ImageSrcInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct1ImageSrcInComparison" stepKey="compareAssertSimpleProduct1ImageNotDefaultInComparison"/> + <!-- Check simple product2 on comparison page --> + <comment userInput="Check simple product 2 on comparison page" stepKey="commentCheckSimpleProduct2OnComparisonPage" /> + <actionGroup ref="StorefrontCheckCompareSimpleProductActionGroup" stepKey="compareAssertSimpleProduct2InComparison"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="compareGrabSimpleProduct2ImageSrcInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabSimpleProduct2ImageSrcInComparison" stepKey="compareAssertSimpleProduct2ImageNotDefaultInComparison"/> + + <!-- Clear comparison sidebar --> + <comment userInput="Clear comparison sidebar" stepKey="commentClearComparisonSidebar" after="compareAssertSimpleProduct2ImageNotDefaultInComparison"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="compareClickCategoryBeforeClear" after="commentClearComparisonSidebar"/> + + + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="compareAssertCategory2"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="compareClearCompare"/> + <comment userInput="End of Comparing Products" stepKey="endOfComparingProducts" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <annotations> + <features value="End to End scenarios"/> + <stories value="B2C guest user - MAGETWO-75411"/> + <group value="e2e"/> + <title value="You should be able to pass End to End B2C Guest User scenario using the Mysql search engine"/> + <description value="User browses catalog, searches for product, adds product to cart, adds product to wishlist, compares products, uses coupon code and checks out using the Mysql search engine."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-20476"/> + <group value="SearchEngineMysql"/> </annotations> <before> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 461ebde29fcad..c8a7cdee66b53 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -49,6 +49,10 @@ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> <waitForPageLoad stepKey="waitForCategorySaved"/> <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index e9e9eb0158789..8092b03c53cba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -53,7 +53,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..3b1cd7ff02e6a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search simple product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search simple product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20361"/> + <group value="Catalog"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="product"/> + </before> + <after> + <deleteData createDataKey="product" stepKey="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..d6b3a060ffd3a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search virtual product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search virtual product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20385"/> + <group value="Catalog"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> + </before> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml new file mode 100644 index 0000000000000..3e72df9133898 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCategoryHighlightedAndProductDisplayedTest"> + <annotations> + <features value="Catalog"/> + <stories value="Category"/> + <title value="Сheck that current category is highlighted and all products displayed for it"/> + <description value="Сheck that current category is highlighted and all products displayed for it"/> + <severity value="MAJOR"/> + <testCaseId value="MC-19626"/> + <useCaseId value="MAGETWO-98748"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SimpleSubCategory" stepKey="category2"/> + <createData entity="SimpleSubCategory" stepKey="category3"/> + <createData entity="SimpleSubCategory" stepKey="category4"/> + <createData entity="SimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="product3"> + <requiredEntity createDataKey="category2"/> + </createData> + <createData entity="SimpleProduct" stepKey="product4"> + <requiredEntity createDataKey="category2"/> + </createData> + </before> + <after> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="product3" stepKey="deleteProduct3"/> + <deleteData createDataKey="product4" stepKey="deleteProduct4"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <deleteData createDataKey="category2" stepKey="deleteCategory2"/> + <deleteData createDataKey="category3" stepKey="deleteCategory3"/> + <deleteData createDataKey="category4" stepKey="deleteCategory4"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open Storefront home page--> + <comment userInput="Open Storefront home page" stepKey="openStorefrontHomePage"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontHomePage"/> + <waitForPageLoad stepKey="waitForSimpleProductPage"/> + <!--Click on first category--> + <comment userInput="Click on first category" stepKey="openFirstCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category1.name$$)}}" stepKey="clickCategory1Name"/> + <waitForPageLoad stepKey="waitForCategory1Page"/> + <!--Check if current category is highlighted and the others are not--> + <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg1NameIsHighlighted"/> + <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category1.name$$)}}" userInput="class" stepKey="grabCategory1Class"/> + <assertContains expectedType="string" expected="active" actual="$grabCategory1Class" stepKey="assertCategory1IsHighlighted"/> + <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount"/> + <assertEquals expectedType="int" expected="1" actual="$highlightedAmount" stepKey="assertRestCategories1IsNotHighlighted"/> + <!--See products in the category page--> + <comment userInput="See products in the category page" stepKey="seeProductsInCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product1.name$)}}" stepKey="seeProduct1InCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product2.name$)}}" stepKey="seeProduct2InCategoryPage"/> + <!--Click on second category--> + <comment userInput="Click on second category" stepKey="openSecondCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$category2.name$$)}}" stepKey="clickCategory2Name"/> + <waitForPageLoad stepKey="waitForCategory2Page"/> + <!--Check if current category is highlighted and the others are not--> + <comment userInput="Check if current category is highlighted and the others are not" stepKey="checkCateg2NameIsHighlighted"/> + <grabAttributeFrom selector="{{AdminCategorySidebarTreeSection.categoryHighlighted($$category2.name$$)}}" userInput="class" stepKey="grabCategory2Class"/> + <assertContains expectedType="string" expected="active" actual="$grabCategory2Class" stepKey="assertCategory2IsHighlighted"/> + <executeJS function="return document.querySelectorAll('{{AdminCategorySidebarTreeSection.categoryNotHighlighted}}').length" stepKey="highlightedAmount2"/> + <assertEquals expectedType="int" expected="1" actual="$highlightedAmount2" stepKey="assertRestCategories1IsNotHighlighted2"/> + <!--Assert products in second category page--> + <comment userInput="Assert products in second category page" stepKey="commentAssertProducts"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product3.name$)}}" stepKey="seeProduct3InCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($product4.name$)}}" stepKey="seeProduct4InCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml index ac2605ff5f3e2..4eef6a2c06800 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -186,6 +186,11 @@ <waitForElementVisible selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" stepKey="waitForSectionOpen"/> <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageAllowedValues}}" userInput="12,24,36" stepKey="seeDefaultValueAllowedNumberProductsPerPage"/> <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageDefaultValue}}" userInput="12" stepKey="seeDefaultValueProductPerPage"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Open storefront on the category page --> <comment userInput="Open storefront on the category page" stepKey="commentOpenStorefront"/> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToStorefrontCreatedCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml index 386633f0e9476..21f8e2e070e32 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml @@ -41,6 +41,9 @@ </actionGroup> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByNameAndSrc(SimpleProductNameWithDoubleQuote.name, ProductImage.fileName)}}" stepKey="seeCorrectImageCategoryPage"/> @@ -88,6 +91,9 @@ <deleteData createDataKey="createCategoryOne" stepKey="deleteCategory"/> </after> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategoryOne.name$$)}}" stepKey="navigateToCategoryPage"/> <waitForPageLoad stepKey="waitforCategoryPageToLoad"/> @@ -111,11 +117,10 @@ <waitForPageLoad stepKey="waitforCategoryPageToLoad2"/> <!--Open product display page--> - <click selector="{{StorefrontCategoryProductSection.ProductTitleByNumber('1')}}" stepKey="goToProduct2DisplayPage"/> - <!--<click selector="{{StorefrontCategoryProductSection.ProductTitleByName(productWithHTMLEntityOne.name)}}" stepKey="clickProductToGoProductPage"/>--> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(productWithHTMLEntityTwo.name)}}" stepKey="clickProductToGoSecondProductPage"/> <waitForPageLoad stepKey="waitForProductDisplayPageLoad3"/> - <!--Veriy the breadcrumbs on Product Display page--> + <!--Verify the breadcrumbs on Product Display page--> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="Home" stepKey="seeHomePageInBreadcrumbs2"/> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$createCategoryOne.name$$" stepKey="seeCorrectBreadCrumbCategory2"/> <see selector="{{StorefrontNavigationSection.breadcrumbs}}" userInput="$$productTwo.name$$" stepKey="seeCorrectBreadCrumbProduct2"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php index 8f3aa66e57c5e..4c3450d555f1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php @@ -17,6 +17,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Ui\DataProvider\Modifier\PoolInterface; +use Magento\Framework\Stdlib\ArrayUtils; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -78,6 +79,14 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $modifierPool; + /** + * @var ArrayUtils|\PHPUnit_Framework_MockObject_MockObject + */ + private $arrayUtils; + + /** + * @inheritDoc + */ protected function setUp() { $this->eavValidationRules = $this->getMockBuilder(EavValidationRules::class) @@ -128,6 +137,10 @@ protected function setUp() ->getMock(); $this->modifierPool = $this->getMockBuilder(PoolInterface::class)->getMockForAbstractClass(); + + $this->arrayUtils = $this->getMockBuilder(ArrayUtils::class) + ->setMethods(['flatten']) + ->disableOriginalConstructor()->getMock(); } /** @@ -157,7 +170,8 @@ private function getModel() 'eavConfig' => $this->eavConfig, 'request' => $this->request, 'categoryFactory' => $this->categoryFactory, - 'pool' => $this->modifierPool + 'pool' => $this->modifierPool, + 'arrayUtils' => $this->arrayUtils ] ); @@ -206,10 +220,12 @@ public function testGetDataNoFileExists() ->getMock(); $categoryMock->expects($this->exactly(2)) ->method('getData') - ->willReturnMap([ - ['', null, $categoryData], - ['image', null, $categoryData['image']], - ]); + ->willReturnMap( + [ + ['', null, $categoryData], + ['image', null, $categoryData['image']], + ] + ); $categoryMock->expects($this->any()) ->method('getExistsStoreValueFlag') ->with('url_key') @@ -280,10 +296,12 @@ public function testGetData() ->getMock(); $categoryMock->expects($this->exactly(2)) ->method('getData') - ->willReturnMap([ - ['', null, $categoryData], - ['image', null, $categoryData['image']], - ]); + ->willReturnMap( + [ + ['', null, $categoryData], + ['image', null, $categoryData['image']], + ] + ); $categoryMock->expects($this->any()) ->method('getExistsStoreValueFlag') ->with('url_key') @@ -331,10 +349,12 @@ public function testGetData() public function testGetMetaWithoutParentInheritanceResolving() { + $this->arrayUtils->expects($this->atLeastOnce())->method('flatten')->willReturn([1,3,3]); + $categoryMock = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor() ->getMock(); - $this->registry->expects($this->once()) + $this->registry->expects($this->atLeastOnce()) ->method('registry') ->with('category') ->willReturn($categoryMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php index 71f5ca33d1303..6977a9ad1c7cc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php @@ -191,6 +191,9 @@ public function testIsExist($fileName, $fileMediaPath) $this->assertTrue($this->model->isExist($fileName)); } + /** + * @return array + */ public function isExistProvider() { return [ @@ -213,6 +216,9 @@ public function testIsBeginsWithMediaDirectoryPath($fileName, $expected) $this->assertEquals($expected, $this->model->isBeginsWithMediaDirectoryPath($fileName)); } + /** + * @return array + */ public function isBeginsWithMediaDirectoryPathProvider() { return [ diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php index 8733f305ce091..731c5efd99746 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Test\Unit\Model\Layer; @@ -72,9 +73,13 @@ public function testGetFilters($method, $value, $expectedClass) $this->objectManagerMock->expects($this->at(1)) ->method('create') - ->with($expectedClass, [ - 'data' => ['attribute_model' => $this->attributeMock], - 'layer' => $this->layerMock]) + ->with( + $expectedClass, + [ + 'data' => ['attribute_model' => $this->attributeMock], + 'layer' => $this->layerMock + ] + ) ->will($this->returnValue('filter')); $this->attributeMock->expects($this->once()) @@ -95,8 +100,8 @@ public function getFiltersDataProvider() { return [ [ - 'method' => 'getAttributeCode', - 'value' => FilterList::PRICE_FILTER, + 'method' => 'getFrontendInput', + 'value' => 'price', 'expectedClass' => 'PriceFilterClass', ], [ @@ -105,8 +110,8 @@ public function getFiltersDataProvider() 'expectedClass' => 'DecimalFilterClass', ], [ - 'method' => 'getAttributeCode', - 'value' => null, + 'method' => 'getFrontendInput', + 'value' => 'text', 'expectedClass' => 'AttributeFilterClass', ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 5eaf2422e95a9..0d1adfe3891e2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -486,6 +486,9 @@ public function testGetStoreSingleSiteModelIds( $this->assertEquals($websiteIDs, $this->model->getStoreIds()); } + /** + * @return array + */ public function getSingleStoreIds() { return [ 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 932b09f7df9cb..bceafee0f82a4 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 @@ -10,7 +10,6 @@ use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; @@ -161,7 +160,14 @@ public function testModifyMetaLocked($locked) ->willReturnArgument(2); $modifyMeta = $this->createModel()->modifyMeta($meta); - $this->assertEquals($locked, $modifyMeta['arguments']['data']['config']['disabled']); + $this->assertEquals( + $locked, + $modifyMeta['children']['category_ids']['arguments']['data']['config']['disabled'] + ); + $this->assertEquals( + $locked, + $modifyMeta['children']['create_category_button']['arguments']['data']['config']['disabled'] + ); } /** 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 a9d717db7b7f9..9d0e7fc57ffce 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 @@ -33,7 +33,7 @@ protected function setUp() parent::setUp(); $this->attributeRepositoryMock = $this->getMockBuilder(AttributeRepositoryInterface::class) - ->getMockForAbstractClass(); + ->getMockForAbstractClass(); $arrayManager = $this->objectManager->getObject(ArrayManager::class); @@ -52,10 +52,13 @@ protected function setUp() */ protected function createModel() { - return $this->objectManager->getObject(General::class, [ + return $this->objectManager->getObject( + General::class, + [ 'locator' => $this->locatorMock, 'arrayManager' => $this->arrayManagerMock, - ]); + ] + ); } public function testModifyMeta() @@ -63,8 +66,10 @@ public function testModifyMeta() $this->arrayManagerMock->expects($this->any()) ->method('merge') ->willReturnArgument(2); - $this->assertNotEmpty($this->getModel()->modifyMeta([ - 'first_panel_code' => [ + $this->assertNotEmpty( + $this->getModel()->modifyMeta( + [ + 'first_panel_code' => [ 'arguments' => [ 'data' => [ 'config' => [ @@ -72,15 +77,17 @@ public function testModifyMeta() ] ], ] - ] - ])); + ] + ] + ) + ); } /** - * @param array $data - * @param int $defaultStatusValue - * @param array $expectedResult - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @param array $data + * @param int $defaultStatusValue + * @param array $expectedResult + * @throws \Magento\Framework\Exception\NoSuchEntityException * @dataProvider modifyDataDataProvider */ public function testModifyDataNewProduct(array $data, int $defaultStatusValue, array $expectedResult) @@ -100,6 +107,97 @@ public function testModifyDataNewProduct(array $data, int $defaultStatusValue, a $this->assertSame($expectedResult, $this->generalModifier->modifyData($data)); } + /** + * Verify the product attribute status set owhen editing existing product + * + * @param array $data + * @param string $modelId + * @param int $defaultStatus + * @param int $statusAttributeValue + * @param array $expectedResult + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider modifyDataOfExistingProductDataProvider + */ + public function testModifyDataOfExistingProduct( + array $data, + string $modelId, + int $defaultStatus, + int $statusAttributeValue, + array $expectedResult + ) { + $attributeMock = $this->getMockForAbstractClass(AttributeInterface::class); + $attributeMock->expects($this->any()) + ->method('getDefaultValue') + ->willReturn($defaultStatus); + $this->attributeRepositoryMock->expects($this->any()) + ->method('get') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ) + ->willReturn($attributeMock); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($modelId); + $this->productMock->expects($this->any()) + ->method('getStatus') + ->willReturn($statusAttributeValue); + $this->assertSame($expectedResult, current($this->generalModifier->modifyData($data))); + } + + /** + * @return array + */ + public function modifyDataOfExistingProductDataProvider(): array + { + return [ + 'With enable status value' => [ + 'data' => [], + 'modelId' => '1', + 'defaultStatus' => 1, + 'statusAttributeValue' => 1, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + 'Without disable status value' => [ + 'data' => [], + 'modelId' => '1', + 'defaultStatus' => 1, + 'statusAttributeValue' => 2, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 2, + ], + ], + ], + 'With enable status value with empty modelId' => [ + 'data' => [], + 'modelId' => '', + 'defaultStatus' => 1, + 'statusAttributeValue' => 1, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + 'Without disable status value with empty modelId' => [ + 'data' => [], + 'modelId' => '', + 'defaultStatus' => 2, + 'statusAttributeValue' => 2, + 'expectedResult' => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 2, + ], + ], + ], + ]; + } + /** * @return array */ diff --git a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php index be73940237db4..932fe4ef33d83 100644 --- a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php +++ b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php @@ -10,6 +10,9 @@ use Magento\Framework\UrlInterface; +/** + * Returns configuration for product Url Input type + */ class Product implements \Magento\Ui\Model\UrlInput\ConfigInterface { /** @@ -27,7 +30,7 @@ public function __construct(UrlInterface $urlBuilder) } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig(): array { @@ -46,6 +49,7 @@ public function getConfig(): array 'template' => 'ui/grid/filters/elements/ui-select', 'searchUrl' => $this->urlBuilder->getUrl('catalog/product/search'), 'filterPlaceholder' => __('Product Name or SKU'), + 'filterRateLimitMethod' => 'notifyWhenChangesStop', 'isDisplayEmptyPlaceholder' => true, 'emptyOptionsHtml' => __('Start typing to find products'), 'missingValuePlaceholder' => __('Product with ID: %s doesn\'t exist'), 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 5f1907344ce83..c4ca5eca8e880 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 @@ -23,7 +23,6 @@ * Data provider for categories field of product page * * @api - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 101.0.0 */ @@ -120,7 +119,7 @@ public function __construct( * @return CacheInterface * @deprecated 101.0.3 */ - private function getCacheManager() + private function getCacheManager(): CacheInterface { if (!$this->cacheManager) { $this->cacheManager = ObjectManager::getInstance() @@ -148,9 +147,9 @@ public function modifyMeta(array $meta) * * @return bool */ - private function isAllowed() + private function isAllowed(): bool { - return $this->authorization->isAllowed('Magento_Catalog::categories'); + return (bool) $this->authorization->isAllowed('Magento_Catalog::categories'); } /** @@ -234,6 +233,7 @@ protected function customizeCategoriesField(array $meta) $fieldCode = 'category_ids'; $elementPath = $this->arrayManager->findPath($fieldCode, $meta, null, 'children'); $containerPath = $this->arrayManager->findPath(static::CONTAINER_PREFIX . $fieldCode, $meta, null, 'children'); + $fieldIsDisabled = $this->locator->getProduct()->isLockedAttribute($fieldCode); if (!$elementPath) { return $meta; @@ -250,7 +250,6 @@ protected function customizeCategoriesField(array $meta) 'componentType' => 'container', 'component' => 'Magento_Ui/js/form/components/group', 'scopeLabel' => __('[GLOBAL]'), - 'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode), ], ], ], @@ -266,6 +265,7 @@ protected function customizeCategoriesField(array $meta) 'chipsEnabled' => true, 'disableLabel' => true, 'levelsVisibility' => '1', + 'disabled' => $fieldIsDisabled, 'elementTmpl' => 'ui/grid/filters/elements/ui-select', 'options' => $this->getCategoriesTree(), 'listens' => [ @@ -291,6 +291,7 @@ protected function customizeCategoriesField(array $meta) 'formElement' => 'container', 'additionalClasses' => 'admin__field-small', 'componentType' => 'container', + 'disabled' => $fieldIsDisabled, 'component' => 'Magento_Ui/js/form/components/button', 'template' => 'ui/form/components/button/container', 'actions' => [ @@ -320,11 +321,7 @@ protected function customizeCategoriesField(array $meta) ] ]; } - $meta = $this->arrayManager->merge( - $containerPath, - $meta, - $value - ); + $meta = $this->arrayManager->merge($containerPath, $meta, $value); return $meta; } 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 5d1e853cef3d1..287b5b514ee8e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -46,6 +46,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) * @since 101.0.0 */ class Eav extends AbstractModifier @@ -725,7 +726,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: getAttributeModel() should not be used when MAGETWO-48284 is complete $childData = $this->arrayManager->get($configPath, $meta, []); - if (($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData))) { + if ($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData)) { $meta = $this->arrayManager->merge($configPath, $meta, ['validation' => $rules]); } @@ -1048,6 +1049,10 @@ private function isScopeGlobal($attribute) */ private function getAttributeModel($attribute) { + // The statement below solves performance issue related to loading same attribute options on different models + if ($attribute instanceof EavAttribute) { + return $attribute; + } $attributeId = $attribute->getAttributeId(); if (!array_key_exists($attributeId, $this->attributesCache)) { 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 91c74a2da5048..7c42b881bad3e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -21,13 +21,13 @@ class General extends AbstractModifier { /** - * @var LocatorInterface + * @var LocatorInterface * @since 101.0.0 */ protected $locator; /** - * @var ArrayManager + * @var ArrayManager * @since 101.0.0 */ protected $arrayManager; @@ -43,8 +43,8 @@ class General extends AbstractModifier private $attributeRepository; /** - * @param LocatorInterface $locator - * @param ArrayManager $arrayManager + * @param LocatorInterface $locator + * @param ArrayManager $arrayManager * @param AttributeRepositoryInterface|null $attributeRepository */ public function __construct( @@ -61,10 +61,10 @@ public function __construct( /** * Customize number fields for advanced price and weight fields. * - * @param array $data + * @param array $data * @return array * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.0.0 + * @since 101.0.0 */ public function modifyData(array $data) { @@ -72,7 +72,10 @@ public function modifyData(array $data) $data = $this->customizeAdvancedPriceFormat($data); $modelId = $this->locator->getProduct()->getId(); - if (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { + $productStatus = $this->locator->getProduct()->getStatus(); + if (!empty($productStatus) && !empty($modelId)) { + $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = $productStatus; + } elseif (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { $attributeStatus = $this->attributeRepository->get( ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_STATUS @@ -87,9 +90,9 @@ public function modifyData(array $data) /** * Customizing weight fields * - * @param array $data + * @param array $data * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeWeightFormat(array $data) { @@ -112,9 +115,9 @@ protected function customizeWeightFormat(array $data) /** * Customizing number fields for advanced price * - * @param array $data + * @param array $data * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeAdvancedPriceFormat(array $data) { @@ -136,9 +139,9 @@ protected function customizeAdvancedPriceFormat(array $data) /** * Customize product form fields. * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ public function modifyMeta(array $meta) { @@ -154,9 +157,9 @@ public function modifyMeta(array $meta) /** * Disable collapsible and set empty label * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function prepareFirstPanel(array $meta) { @@ -177,9 +180,9 @@ protected function prepareFirstPanel(array $meta) /** * Customize Status field * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeStatusField(array $meta) { @@ -203,9 +206,9 @@ protected function customizeStatusField(array $meta) /** * Customize Weight filed * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeWeightField(array $meta) { @@ -277,9 +280,9 @@ protected function customizeWeightField(array $meta) /** * Customize "Set Product as New" date fields * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeNewDateRangeField(array $meta) { @@ -335,9 +338,9 @@ protected function customizeNewDateRangeField(array $meta) /** * Add links for fields depends of product name * - * @param array $meta + * @param array $meta * @return array - * @since 101.0.0 + * @since 101.0.0 */ protected function customizeNameListeners(array $meta) { @@ -409,9 +412,9 @@ private function getLocaleCurrency() /** * Format price according to the locale of the currency * - * @param mixed $value + * @param mixed $value * @return string - * @since 101.0.0 + * @since 101.0.0 */ protected function formatPrice($value) { @@ -429,9 +432,9 @@ protected function formatPrice($value) /** * Format number according to the locale of the currency and precision of input * - * @param mixed $value + * @param mixed $value * @return string - * @since 101.0.0 + * @since 101.0.0 */ protected function formatNumber($value) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php new file mode 100644 index 0000000000000..01aaa6f8e0629 --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Modifier/Attributes.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Modifier; + +use Magento\Framework\Escaper; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; + +/** + * Modify product listing attributes + */ +class Attributes implements ModifierInterface +{ + /** + * @var Escaper + */ + private $escaper; + + /** + * @var array + */ + private $escapeAttributes; + + /** + * @param Escaper $escaper + * @param array $escapeAttributes + */ + public function __construct( + Escaper $escaper, + array $escapeAttributes = [] + ) { + $this->escaper = $escaper; + $this->escapeAttributes = $escapeAttributes; + } + + /** + * @inheritdoc + */ + public function modifyData(array $data) + { + if (!empty($data) && !empty($this->escapeAttributes)) { + foreach ($data['items'] as &$item) { + foreach ($this->escapeAttributes as $escapeAttribute) { + if (isset($item[$escapeAttribute])) { + $item[$escapeAttribute] = $this->escaper->escapeHtml($item[$escapeAttribute]); + } + } + } + } + return $data; + } + + /** + * @inheritdoc + */ + public function modifyMeta(array $meta) + { + return $meta; + } +} diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index c04cfb2dce00a..b3c9230b3cfc6 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -166,7 +166,24 @@ <argument name="pool" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool</argument> </arguments> </type> - <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Listing\Modifier\Pool" type="Magento\Ui\DataProvider\Modifier\Pool"/> + <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Listing\Modifier\Pool" type="Magento\Ui\DataProvider\Modifier\Pool"> + <arguments> + <argument name="modifiers" xsi:type="array"> + <item name="attributes" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Catalog\Ui\DataProvider\Product\Modifier\Attributes</item> + <item name="sortOrder" xsi:type="number">10</item> + </item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Ui\DataProvider\Product\Modifier\Attributes"> + <arguments> + <argument name="escapeAttributes" xsi:type="array"> + <item name="name" xsi:type="string">name</item> + <item name="sku" xsi:type="string">sku</item> + </argument> + </arguments> + </type> <type name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CustomOptions"> <arguments> <argument name="scopeName" xsi:type="string">product_form.product_form</argument> diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml index ad83f5898237a..ab1a8348d2904 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/events.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml @@ -12,4 +12,7 @@ <event name="catalog_category_change_products"> <observer name="category_product_indexer" instance="Magento\Catalog\Observer\CategoryProductIndexer"/> </event> + <event name="category_move"> + <observer name="clean_cagegory_page_cache" instance="Magento\Catalog\Observer\FlushCategoryPagesCache" /> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index 6fef4ca6e9128..d5b318f671726 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -812,7 +812,7 @@ <column xsi:type="smallint" name="disabled" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is Disabled"/> <column xsi:type="int" name="record_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Record Id"/> + comment="Record ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="record_id"/> </constraint> @@ -1085,7 +1085,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1114,7 +1114,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1205,7 +1205,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> <column xsi:type="smallint" name="default_store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Default store id for website"/> + comment="Default store ID for website"/> <column xsi:type="date" name="website_date" comment="Website Date"/> <column xsi:type="float" name="rate" unsigned="false" nullable="true" default="1" comment="Rate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -1238,7 +1238,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_cfg_opt_agr_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_cfg_opt_agr_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Config Option Aggregate Temp Table"> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent ID"/> @@ -1279,7 +1279,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_cfg_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_cfg_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Config Option Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1327,7 +1327,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_final_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_final_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Final Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1375,7 +1375,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_opt_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_opt_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Option Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1418,7 +1418,7 @@ <column name="option_id"/> </constraint> </table> - <table name="catalog_product_index_price_opt_agr_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_opt_agr_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Option Aggregate Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1452,7 +1452,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1470,7 +1470,7 @@ <column name="value"/> </index> </table> - <table name="catalog_product_index_eav_tmp" resource="default" engine="memory" + <table name="catalog_product_index_eav_tmp" resource="default" engine="innodb" comment="Catalog Product EAV Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1481,7 +1481,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1489,13 +1489,13 @@ <column name="value"/> <column name="source_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1510,7 +1510,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1528,7 +1528,7 @@ <column name="value"/> </index> </table> - <table name="catalog_product_index_eav_decimal_tmp" resource="default" engine="memory" + <table name="catalog_product_index_eav_decimal_tmp" resource="default" engine="innodb" comment="Catalog Product EAV Decimal Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1539,7 +1539,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1547,13 +1547,13 @@ <column name="value"/> <column name="source_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1592,7 +1592,7 @@ <column name="min_price"/> </index> </table> - <table name="catalog_product_index_price_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_tmp" resource="default" engine="innodb" comment="Catalog Product Price Indexer Temp Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -1617,17 +1617,17 @@ <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="btree"> <column name="min_price"/> </index> </table> - <table name="catalog_category_product_index_tmp" resource="default" engine="memory" + <table name="catalog_category_product_index_tmp" resource="default" engine="innodb" comment="Catalog Category Product Indexer temporary table"> <column xsi:type="int" name="category_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Category ID"/> @@ -1646,7 +1646,7 @@ <column name="product_id"/> <column name="store_id"/> </constraint> - <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> + <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="btree"> <column name="product_id"/> <column name="category_id"/> <column name="store_id"/> @@ -1681,7 +1681,7 @@ <column xsi:type="int" name="value" padding="10" unsigned="true" nullable="false" identity="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1710,7 +1710,7 @@ <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Original entity Id for attribute value"/> + default="0" comment="Original entity ID for attribute value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> @@ -1801,14 +1801,14 @@ <table name="catalog_product_frontend_action" resource="default" engine="innodb" comment="Catalog Product Frontend Action Table"> <column xsi:type="bigint" name="action_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Product Action Id"/> + comment="Product Action ID"/> <column xsi:type="varchar" name="type_id" nullable="false" length="64" comment="Type of product action"/> <column xsi:type="int" name="visitor_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Visitor Id"/> + comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="bigint" name="added_at" padding="20" unsigned="false" nullable="false" identity="false" comment="Added At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index d4d20995a48b4..ff2fab73e0379 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -36,16 +36,16 @@ <preference for="Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface" type="Magento\Catalog\Model\ProductAttributeGroupRepository" /> <preference for="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> <preference for="Magento\Catalog\Api\ProductLinkRepositoryInterface" type="Magento\Catalog\Model\ProductLink\Repository" /> - <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Catalog\Api\Data\ProductSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Catalog\Model\ProductAttributeSearchResults" /> + <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Catalog\Model\CategoryAttributeSearchResults" /> + <preference for="Magento\Catalog\Api\Data\ProductSearchResultsInterface" type="Magento\Catalog\Model\ProductSearchResults" /> <preference for="Magento\Catalog\Api\ProductAttributeManagementInterface" type="Magento\Catalog\Model\Product\Attribute\Management" /> <preference for="Magento\Catalog\Api\AttributeSetManagementInterface" type="Magento\Catalog\Model\Product\Attribute\SetManagement" /> <preference for="Magento\Catalog\Api\AttributeSetRepositoryInterface" type="Magento\Catalog\Model\Product\Attribute\SetRepository" /> <preference for="Magento\Catalog\Api\ProductManagementInterface" type="Magento\Catalog\Model\ProductManagement" /> <preference for="Magento\Catalog\Api\AttributeSetFinderInterface" type="Magento\Catalog\Model\Product\Attribute\AttributeSetFinder" /> <preference for="Magento\Catalog\Api\CategoryListInterface" type="Magento\Catalog\Model\CategoryList" /> - <preference for="Magento\Catalog\Api\Data\CategorySearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Catalog\Api\Data\CategorySearchResultsInterface" type="Magento\Catalog\Model\CategorySearchResults" /> <preference for="Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface" type="Magento\Catalog\Model\Config\Source\Product\Options\Price"/> <preference for="Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterface" type="Magento\Catalog\Model\Indexer\Product\Flat\Table\Builder"/> <preference for="Magento\Catalog\Api\ProductRenderListInterface" type="Magento\Catalog\Model\ProductRenderList"/> @@ -867,7 +867,7 @@ <argument name="hydrators" xsi:type="array"> <item name="Magento\Catalog\Api\Data\CategoryInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> <item name="Magento\Catalog\Api\Data\CategoryTreeInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> - <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="string">Magento\Framework\EntityManager\AbstractModelHydrator</item> + <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="string">Magento\Catalog\Model\Product\Hydrator</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 6b63a20134df1..d6340330df8ea 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -507,8 +507,13 @@ })(jQuery); this.closeModal(); } - }] - + }], + keyEventHandlers: { + enterKey: function (event) { + this.buttons[1].click(); + event.preventDefault(); + } + } }).trigger('openModal'); } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index ce44884a575b8..8c32302cf7c29 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -72,7 +72,9 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); </strong> <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> <?= /* @noEscape */ $block->getProductPrice($_product) ?> - <?= $block->getProductDetailsHtml($_product) ?> + <?php if ($_product->isAvailable()) :?> + <?= $block->getProductDetailsHtml($_product) ?> + <?php endif; ?> <div class="product-item-inner"> <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $block->escapeHtmlAttr($position) : '' ?>> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js index f6be6fd58ca25..2b3349c25c917 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js @@ -3,18 +3,10 @@ * See COPYING.txt for license details. */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'jquery-ui-modules/widget' - ], factory); - } else { - factory(jQuery); - } -}(function ($) { +define([ + 'jquery', + 'jquery-ui-modules/widget' +], function ($) { 'use strict'; $.widget('mage.gallery', { @@ -49,4 +41,4 @@ }); return $.mage.gallery; -})); +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js index ab1753e7b9ed3..3205e58297b6c 100644 --- a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js +++ b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js @@ -3,19 +3,11 @@ * See COPYING.txt for license details. */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'jquery-ui-modules/widget', - 'mage/validation/validation' - ], factory); - } else { - factory(jQuery); - } -}(function ($) { +define([ + 'jquery', + 'jquery-ui-modules/widget', + 'mage/validation/validation' +], function ($) { 'use strict'; $.widget('mage.validation', $.mage.validation, { @@ -97,4 +89,4 @@ }); return $.mage.validation; -})); +}); diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php new file mode 100644 index 0000000000000..65ab2940a7a51 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Customer/GetCustomerGroup.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver\Customer; + +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\GroupManagement; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; + +/** + * Get customer group + */ +class GetCustomerGroup +{ + /** + * @var GroupManagementInterface + */ + private $groupManagement; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @param GroupManagementInterface $groupManagement + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + GroupManagementInterface $groupManagement, + CustomerRepositoryInterface $customerRepository + ) { + $this->groupManagement = $groupManagement; + $this->customerRepository = $customerRepository; + } + + /** + * Get customer group by id + * + * @param int|null $customerId + * @return int + * @throws GraphQlNoSuchEntityException + */ + public function execute(?int $customerId): int + { + if (!$customerId) { + $customerGroupId = GroupManagement::NOT_LOGGED_IN_ID; + } else { + try { + $customer = $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), + $e + ); + } + $customerGroupId = $customer->getGroupId(); + } + return (int)$customerGroupId; + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php new file mode 100644 index 0000000000000..4e75139c1a882 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; +use Magento\Store\Api\Data\StoreInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; + +/** + * Resolver for price_tiers + */ +class PriceTiers implements ResolverInterface +{ + /** + * @var TiersFactory + */ + private $tiersFactory; + + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var GetCustomerGroup + */ + private $getCustomerGroup; + + /** + * @var int + */ + private $customerGroupId; + + /** + * @var Tiers + */ + private $tiers; + + /** + * @var Discount + */ + private $discount; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @param ValueFactory $valueFactory + * @param TiersFactory $tiersFactory + * @param GetCustomerGroup $getCustomerGroup + * @param Discount $discount + * @param PriceProviderPool $priceProviderPool + */ + public function __construct( + ValueFactory $valueFactory, + TiersFactory $tiersFactory, + GetCustomerGroup $getCustomerGroup, + Discount $discount, + PriceProviderPool $priceProviderPool + ) { + $this->valueFactory = $valueFactory; + $this->tiersFactory = $tiersFactory; + $this->getCustomerGroup = $getCustomerGroup; + $this->discount = $discount; + $this->priceProviderPool = $priceProviderPool; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (empty($this->tiers)) { + $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId()); + $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]); + } + + $product = $value['model']; + $productId = $product->getId(); + $this->tiers->addProductFilter($productId); + + return $this->valueFactory->create( + function () use ($productId, $context) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + $productPrice = $this->tiers->getProductRegularPrice($productId) ?? 0.0; + $tierPrices = $this->tiers->getProductTierPrices($productId) ?? []; + + return $this->formatProductTierPrices($tierPrices, $productPrice, $store); + } + ); + } + + /** + * Format tier prices for output + * + * @param ProductTierPriceInterface[] $tierPrices + * @param float $productPrice + * @param StoreInterface $store + * @return array + */ + private function formatProductTierPrices(array $tierPrices, float $productPrice, StoreInterface $store): array + { + $tiers = []; + + foreach ($tierPrices as $tierPrice) { + $percentValue = $tierPrice->getExtensionAttributes()->getPercentageValue(); + if ($percentValue && is_numeric($percentValue)) { + $discount = $this->discount->getDiscountByPercent($productPrice, (float)$percentValue); + } else { + $discount = $this->discount->getDiscountByDifference($productPrice, (float)$tierPrice->getValue()); + } + + $tiers[] = [ + "discount" => $discount, + "quantity" => $tierPrice->getQty(), + "final_price" => [ + "value" => $tierPrice->getValue(), + "currency" => $store->getCurrentCurrencyCode() + ] + ]; + } + return $tiers; + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php new file mode 100644 index 0000000000000..73a2ba83d5096 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Customer\Model\GroupManagement; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; + +/** + * Get product tier price information + */ +class Tiers +{ + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var ProductResource + */ + private $productResource; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @var bool + */ + private $loaded = false; + + /** + * @var int + */ + private $customerGroupId = GroupManagement::CUST_GROUP_ALL; + + /** + * @var array + */ + private $filterProductIds = []; + + /** + * @var array + */ + private $products = []; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductResource $productResource + * @param PriceProviderPool $priceProviderPool + * @param int $customerGroupId + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductResource $productResource, + PriceProviderPool $priceProviderPool, + $customerGroupId + ) { + $this->collectionFactory = $collectionFactory; + $this->productResource = $productResource; + $this->priceProviderPool = $priceProviderPool; + $this->customerGroupId = $customerGroupId; + } + + /** + * Add product ID to collection filter + * + * @param int $productId + */ + public function addProductFilter($productId): void + { + $this->filterProductIds[] = $productId; + } + + /** + * Get tier prices for product by ID + * + * @param int $productId + * @return ProductTierPriceInterface[]|null + */ + public function getProductTierPrices($productId): ?array + { + if (!$this->isLoaded()) { + $this->load(); + } + + if (empty($this->products[$productId])) { + return null; + } + return $this->products[$productId]->getTierPrices(); + } + + /** + * Get product regular price by ID + * + * @param int $productId + * @return float|null + */ + public function getProductRegularPrice($productId): ?float + { + if (!$this->isLoaded()) { + $this->load(); + } + + if (empty($this->products[$productId])) { + return null; + } + $product = $this->products[$productId]; + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + return $priceProvider->getRegularPrice($product)->getValue(); + } + + /** + * Check if collection has been loaded + * + * @return bool + */ + public function isLoaded(): bool + { + $numFilterProductIds = count(array_unique($this->filterProductIds)); + if ($numFilterProductIds > count($this->products)) { + //New products were added to the filter after load, so we should reload + return false; + } + return $this->loaded; + } + + /** + * Load product collection + */ + private function load(): void + { + $this->loaded = false; + + $productIdField = $this->productResource->getEntityIdField(); + /** @var Collection $productCollection */ + $productCollection = $this->collectionFactory->create(); + $productCollection->addFieldToFilter($productIdField, ['in' => $this->filterProductIds]); + $productCollection->addAttributeToSelect('price'); + $productCollection->addAttributeToSelect('price_type'); + $productCollection->load(); + $productCollection->addTierPriceDataByGroupId($this->customerGroupId); + + $this->setProducts($productCollection); + $this->loaded = true; + } + + /** + * Set products from collection + * + * @param Collection $productCollection + */ + private function setProducts(Collection $productCollection): void + { + $this->products = []; + + foreach ($productCollection as $product) { + $this->products[$product->getId()] = $product; + } + + $missingProducts = array_diff($this->filterProductIds, array_keys($this->products)); + foreach (array_unique($missingProducts) as $missingProductId) { + $this->products[$missingProductId] = null; + } + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php new file mode 100644 index 0000000000000..c449d0a2ba30b --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogCustomerGraphQl\Model\Resolver; + +use Magento\Catalog\Model\Product; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; +use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; + +/** + * @inheritdoc + */ +class TierPrices implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var int + */ + private $customerGroupId = null; + + /** + * @var Tiers + */ + private $tiers; + + /** + * @var TiersFactory + */ + private $tiersFactory; + + /** + * @var GetCustomerGroup + */ + private $getCustomerGroup; + + /** + * @param ValueFactory $valueFactory + * @param TiersFactory $tiersFactory + * @param GetCustomerGroup $getCustomerGroup + */ + public function __construct( + ValueFactory $valueFactory, + TiersFactory $tiersFactory, + GetCustomerGroup $getCustomerGroup + ) { + $this->valueFactory = $valueFactory; + $this->tiersFactory = $tiersFactory; + $this->getCustomerGroup = $getCustomerGroup; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (null === $this->customerGroupId) { + $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId()); + $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]); + } + + /** @var Product $product */ + $product = $value['model']; + $productId = $product->getId(); + $this->tiers->addProductFilter($productId); + + return $this->valueFactory->create( + function () use ($productId, $context) { + $tierPrices = $this->tiers->getProductTierPrices($productId); + + return $tierPrices ?? []; + } + ); + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md new file mode 100644 index 0000000000000..525a1a4f76433 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/README.md @@ -0,0 +1,3 @@ +# CatalogCustomerGraphQl + +**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules. \ No newline at end of file diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json new file mode 100644 index 0000000000000..859a5c6235697 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-catalog-customer-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-store": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogCustomerGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml b/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml new file mode 100644 index 0000000000000..6131435258b58 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_CatalogCustomerGraphQl" > + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_Customer"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..17880584bf160 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls @@ -0,0 +1,22 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + tier_prices: [ProductTierPrices] @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\TierPrices") + price_tiers: [TierPrice] @doc(description: "An array of TierPrice objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\PriceTiers") +} + +type ProductTierPrices @doc(description: "ProductTierPrices is deprecated and has been replaced by TierPrice. The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") { + customer_group_id: String @deprecated(reason: "customer_group_id is not relevant for storefront.") @doc(description: "The ID of the customer group.") + qty: Float @deprecated(reason: "ProductTierPrices is deprecated, use TierPrice.quantity.") @doc(description: "The number of items that must be purchased to qualify for tier pricing.") + value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.final_price") @doc(description: "The price of the fixed price item.") + percentage_value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.discount.") @doc(description: "The percentage discount of the item.") + website_id: Float @deprecated(reason: "website_id is not relevant for storefront.") @doc(description: "The ID assigned to the website.") +} + + +type TierPrice @doc(description: "A price based on the quantity purchased.") { + final_price: Money @doc(desription: "The price of the product at this tier.") + quantity: Float @doc(description: "The minimum number of items that must be purchased to qualify for this price tier.") + discount: ProductDiscount @doc(description: "The price discount that this tier represents.") +} diff --git a/app/code/Magento/CatalogCustomerGraphQl/registration.php b/app/code/Magento/CatalogCustomerGraphQl/registration.php new file mode 100644 index 0000000000000..8176716d42ea0 --- /dev/null +++ b/app/code/Magento/CatalogCustomerGraphQl/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CatalogCustomerGraphQl', __DIR__); diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php new file mode 100644 index 0000000000000..2c03550404ae0 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Category; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Search\Model\Query; + +/** + * Category filter allows to filter collection using 'id, url_key, name' from search criteria. + */ +class CategoryFilter +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param Collection $categoryCollection + * @param StoreInterface $store + * @throws InputException + */ + public function applyFilters(array $args, Collection $categoryCollection, StoreInterface $store) + { + $categoryCollection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, ['eq' => 1]); + foreach ($args['filters'] as $field => $cond) { + foreach ($cond as $condType => $value) { + if ($field === 'ids') { + $categoryCollection->addIdFilter($value); + } else { + $this->addAttributeFilter($categoryCollection, $field, $condType, $value, $store); + } + } + } + } + + /** + * Add filter to category collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $condType + * @param string|array $value + * @param StoreInterface $store + * @throws InputException + */ + private function addAttributeFilter($categoryCollection, $field, $condType, $value, $store) + { + if ($condType === 'match') { + $this->addMatchFilter($categoryCollection, $field, $value, $store); + return; + } + $categoryCollection->addAttributeToFilter($field, [$condType => $value]); + } + + /** + * Add match filter to collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $value + * @param StoreInterface $store + * @throws InputException + */ + private function addMatchFilter($categoryCollection, $field, $value, $store) + { + $minQueryLength = $this->scopeConfig->getValue( + Query::XML_PATH_MIN_QUERY_LENGTH, + ScopeInterface::SCOPE_STORE, + $store + ); + $searchValue = str_replace('%', '', $value); + $matchLength = strlen($searchValue); + if ($matchLength < $minQueryLength) { + throw new InputException(__('Invalid match filter')); + } + + $categoryCollection->addAttributeToFilter($field, ['like' => "%{$searchValue}%"]); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php index 9e23c4f1e9736..863e621bd8df3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php @@ -29,8 +29,11 @@ public function __construct( } /** + * Get breadcrumbs data + * * @param string $categoryPath * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getData(string $categoryPath): array { @@ -41,7 +44,7 @@ public function getData(string $categoryPath): array if (count($parentCategoryIds)) { $collection = $this->collectionFactory->create(); - $collection->addAttributeToSelect(['name', 'url_key']); + $collection->addAttributeToSelect(['name', 'url_key', 'url_path']); $collection->addAttributeToFilter('entity_id', $parentCategoryIds); foreach ($collection as $category) { @@ -50,6 +53,7 @@ public function getData(string $categoryPath): array 'category_name' => $category->getName(), 'category_level' => $category->getLevel(), 'category_url_key' => $category->getUrlKey(), + 'category_url_path' => $category->getUrlPath(), ]; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php new file mode 100644 index 0000000000000..6b8949d612829 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; +use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; + +/** + * Category List resolver, used for GraphQL category data request processing. + */ +class CategoryList implements ResolverInterface +{ + /** + * @var CategoryTree + */ + private $categoryTree; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var CategoryFilter + */ + private $categoryFilter; + + /** + * @var ExtractDataFromCategoryTree + */ + private $extractDataFromCategoryTree; + + /** + * @param CategoryTree $categoryTree + * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree + * @param CategoryFilter $categoryFilter + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CategoryTree $categoryTree, + ExtractDataFromCategoryTree $extractDataFromCategoryTree, + CategoryFilter $categoryFilter, + CollectionFactory $collectionFactory + ) { + $this->categoryTree = $categoryTree; + $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; + $this->categoryFilter = $categoryFilter; + $this->collectionFactory = $collectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (isset($value[$field->getName()])) { + return $value[$field->getName()]; + } + $store = $context->getExtensionAttributes()->getStore(); + + $rootCategoryIds = []; + if (!isset($args['filters'])) { + $rootCategoryIds[] = (int)$store->getRootCategoryId(); + } else { + $categoryCollection = $this->collectionFactory->create(); + try { + $this->categoryFilter->applyFilters($args, $categoryCollection, $store); + } catch (InputException $e) { + return []; + } + + foreach ($categoryCollection as $category) { + $rootCategoryIds[] = (int)$category->getId(); + } + } + + $result = $this->fetchCategories($rootCategoryIds, $info); + return $result; + } + + /** + * Fetch category tree data + * + * @param array $categoryIds + * @param ResolveInfo $info + * @return array + * @throws GraphQlNoSuchEntityException + */ + private function fetchCategories(array $categoryIds, ResolveInfo $info) + { + $fetchedCategories = []; + foreach ($categoryIds as $categoryId) { + $categoryTree = $this->categoryTree->getTree($info, $categoryId); + if (empty($categoryTree)) { + continue; + } + $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree)); + } + + return $fetchedCategories; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php new file mode 100644 index 0000000000000..c56e05bf267a4 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +/** + * Calculate price discount as value and percent + */ +class Discount +{ + /** + * @var float + */ + private $zeroThreshold = 0.0001; + + /** + * Get formatted discount between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return array + */ + public function getDiscountByDifference(float $regularPrice, float $finalPrice): array + { + return [ + 'amount_off' => $this->getPriceDifferenceAsValue($regularPrice, $finalPrice), + 'percent_off' => $this->getPriceDifferenceAsPercent($regularPrice, $finalPrice) + ]; + } + + /** + * Get formatted discount based on percent off + * + * @param float $regularPrice + * @param float $percentOff + * @return array + */ + public function getDiscountByPercent(float $regularPrice, float $percentOff): array + { + return [ + 'amount_off' => $this->getPercentDiscountAsValue($regularPrice, $percentOff), + 'percent_off' => $percentOff + ]; + } + + /** + * Get value difference between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return float + */ + private function getPriceDifferenceAsValue(float $regularPrice, float $finalPrice): float + { + $difference = $regularPrice - $finalPrice; + if ($difference <= $this->zeroThreshold) { + return 0; + } + return round($difference, 2); + } + + /** + * Get percent difference between two prices + * + * @param float $regularPrice + * @param float $finalPrice + * @return float + */ + private function getPriceDifferenceAsPercent(float $regularPrice, float $finalPrice): float + { + $difference = $this->getPriceDifferenceAsValue($regularPrice, $finalPrice); + + if ($difference <= $this->zeroThreshold || $regularPrice <= $this->zeroThreshold) { + return 0; + } + + return round(($difference / $regularPrice) * 100, 2); + } + + /** + * Get amount difference that percentOff represents + * + * @param float $regularPrice + * @param float $percentOff + * @return float + */ + private function getPercentDiscountAsValue(float $regularPrice, float $percentOff): float + { + $percentDecimal = $percentOff / 100; + $valueDiscount = $regularPrice * $percentDecimal; + + return round($valueDiscount, 2); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..67dbcf861170f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides product prices + */ +class Provider implements ProviderInterface +{ + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + /** @var FinalPrice $finalPrice */ + $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + return $finalPrice->getMinimalPrice(); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getRegularPrice($product); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + /** @var FinalPrice $finalPrice */ + $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE); + return $finalPrice->getMaximalPrice(); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getRegularPrice($product); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php new file mode 100644 index 0000000000000..99459daf045a5 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; + +/** + * Provides product prices + */ +interface ProviderInterface +{ + /** + * Get the product minimal final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product minimal regular price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product maximum final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product maximum final price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface; + + /** + * Get the product regular price + * + * @param SaleableInterface $product + * @return AmountInterface + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface; +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php new file mode 100644 index 0000000000000..a23c28a868b6c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderPool.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\Price; + +/** + * Pool of price providers for different product types + */ +class ProviderPool +{ + private const DEFAULT = 'default'; + + /** + * @var ProviderInterface[] + */ + private $providers; + + /** + * @param ProviderInterface[] $providers + */ + public function __construct(array $providers) + { + $this->providers = $providers; + } + + /** + * Get price provider by product type + * + * @param string $productType + * @return ProviderInterface + */ + public function getProviderByProductType(string $productType): ProviderInterface + { + if (isset($this->providers[$productType])) { + return $this->providers[$productType]; + } + return $this->providers[self::DEFAULT]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php new file mode 100644 index 0000000000000..9396b1f02b975 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Format product's pricing information for price_range field + */ +class PriceRange implements ResolverInterface +{ + /** + * @var Discount + */ + private $discount; + + /** + * @var PriceProviderPool + */ + private $priceProviderPool; + + /** + * @param PriceProviderPool $priceProviderPool + * @param Discount $discount + */ + public function __construct(PriceProviderPool $priceProviderPool, Discount $discount) + { + $this->priceProviderPool = $priceProviderPool; + $this->discount = $discount; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var Product $product */ + $product = $value['model']; + $product->unsetData('minimal_price'); + + $requestedFields = $info->getFieldSelection(10); + $returnArray = []; + + if (isset($requestedFields['minimum_price'])) { + $returnArray['minimum_price'] = $this->getMinimumProductPrice($product, $store); + } + if (isset($requestedFields['maximum_price'])) { + $returnArray['maximum_price'] = $this->getMaximumProductPrice($product, $store); + } + return $returnArray; + } + + /** + * Get formatted minimum product price + * + * @param SaleableInterface $product + * @param StoreInterface $store + * @return array + */ + private function getMinimumProductPrice(SaleableInterface $product, StoreInterface $store): array + { + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + $regularPrice = $priceProvider->getMinimalRegularPrice($product)->getValue(); + $finalPrice = $priceProvider->getMinimalFinalPrice($product)->getValue(); + $minPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store); + $minPriceArray['model'] = $product; + return $minPriceArray; + } + + /** + * Get formatted maximum product price + * + * @param SaleableInterface $product + * @param StoreInterface $store + * @return array + */ + private function getMaximumProductPrice(SaleableInterface $product, StoreInterface $store): array + { + $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId()); + $regularPrice = $priceProvider->getMaximalRegularPrice($product)->getValue(); + $finalPrice = $priceProvider->getMaximalFinalPrice($product)->getValue(); + $maxPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store); + $maxPriceArray['model'] = $product; + return $maxPriceArray; + } + + /** + * Format price for GraphQl output + * + * @param float $regularPrice + * @param float $finalPrice + * @param StoreInterface $store + * @return array + */ + private function formatPrice(float $regularPrice, float $finalPrice, StoreInterface $store): array + { + return [ + 'regular_price' => [ + 'value' => $regularPrice, + 'currency' => $store->getCurrentCurrencyCode() + ], + 'final_price' => [ + 'value' => $finalPrice, + 'currency' => $store->getCurrentCurrencyCode() + ], + 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice), + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php deleted file mode 100644 index 726ef91c56880..0000000000000 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogGraphQl\Model\Resolver\Product; - -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\TierPrice; -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; - -/** - * @inheritdoc - * - * Format a product's tier price information to conform to GraphQL schema representation - */ -class TierPrices implements ResolverInterface -{ - /** - * @inheritdoc - * - * Format product's tier price data to conform to GraphQL schema - * - * @param \Magento\Framework\GraphQl\Config\Element\Field $field - * @param ContextInterface $context - * @param ResolveInfo $info - * @param array|null $value - * @param array|null $args - * @throws \Exception - * @return null|array - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - array $value = null, - array $args = null - ) { - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } - - /** @var Product $product */ - $product = $value['model']; - - $tierPrices = null; - if ($product->getTierPrices()) { - $tierPrices = []; - /** @var TierPrice $tierPrice */ - foreach ($product->getTierPrices() as $tierPrice) { - $tierPrices[] = $tierPrice->getData(); - } - } - - return $tierPrices; - } -} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php index f4cefeb3f3638..2ad05fbfa1e08 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php @@ -19,7 +19,22 @@ class AttributeProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Map GraphQl input fields to product attributes + * + * @var array + */ + private $fieldToAttributeMap = []; + + /** + * @param array $fieldToAttributeMap + */ + public function __construct($fieldToAttributeMap = []) + { + $this->fieldToAttributeMap = array_merge($this->fieldToAttributeMap, $fieldToAttributeMap); + } + + /** + * @inheritdoc */ public function process( Collection $collection, @@ -27,9 +42,32 @@ public function process( array $attributeNames ): Collection { foreach ($attributeNames as $name) { - $collection->addAttributeToSelect($name); + $this->addAttribute($collection, $name); } return $collection; } + + /** + * Add attribute to collection select + * + * @param Collection $collection + * @param string $attribute + */ + private function addAttribute(Collection $collection, string $attribute): void + { + if (isset($this->fieldToAttributeMap[$attribute])) { + $attributeMap = $this->fieldToAttributeMap[$attribute]; + if (is_array($attributeMap)) { + foreach ($attributeMap as $attributeName) { + $collection->addAttributeToSelect($attributeName); + } + } else { + $collection->addAttributeToSelect($attributeMap); + } + + } else { + $collection->addAttributeToSelect($attribute); + } + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php index 3912bab05ebbe..ae4f2e911a5b0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php @@ -60,9 +60,9 @@ private function getProductFields(ResolveInfo $info): array $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); } } - - $fieldNames = array_merge(...$fieldNames); - + if (!empty($fieldNames)) { + $fieldNames = array_merge(...$fieldNames); + } return $fieldNames; } diff --git a/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php new file mode 100644 index 0000000000000..cfb99ce270c21 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Plugin; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\View\DesignLoader as ViewDesignLoader; +use Magento\Framework\Message\ManagerInterface; +use Magento\Catalog\Block\Product\ImageFactory; + +/** + * Load necessary design files for GraphQL + */ +class DesignLoader +{ + /** + * @var DesignLoader + */ + private $designLoader; + + /** + * @var ManagerInterface + */ + private $messageManager; + + /** + * @param ViewDesignLoader $designLoader + * @param ManagerInterface $messageManager + */ + public function __construct( + ViewDesignLoader $designLoader, + ManagerInterface $messageManager + ) { + $this->designLoader = $designLoader; + $this->messageManager = $messageManager; + } + + /** + * Before create load the design files + * + * @param ImageFactory $subject + * @param Product $product + * @param string $imageId + * @param array|null $attributes + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeCreate( + ImageFactory $subject, + Product $product, + string $imageId, + array $attributes = null + ) { + try { + $this->designLoader->load(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) { + /** @var MessageInterface $message */ + $message = $this->messageManager + ->createMessage(MessageInterface::TYPE_ERROR) + ->setText($e->getMessage()); + $this->messageManager->addUniqueMessages([$message]); + } + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php new file mode 100644 index 0000000000000..5ebb48f761c06 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Product\Price; + +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; +use PHPUnit\Framework\TestCase; + +class DiscountTest extends TestCase +{ + /** + * @var Discount + */ + private $discount; + + protected function setUp() + { + $this->discount = new Discount(); + } + + /** + * @dataProvider priceDataProvider + * @param $regularPrice + * @param $finalPrice + * @param $expectedAmountOff + * @param $expectedPercentOff + */ + public function testGetPriceDiscount($regularPrice, $finalPrice, $expectedAmountOff, $expectedPercentOff) + { + $discountResult = $this->discount->getDiscountByDifference($regularPrice, $finalPrice); + + $this->assertEquals($expectedAmountOff, $discountResult['amount_off']); + $this->assertEquals($expectedPercentOff, $discountResult['percent_off']); + } + + /** + * Price data provider + * + * [regularPrice, finalPrice, expectedAmountOff, expectedPercentOff] + * + * @return array + */ + public function priceDataProvider() + { + return [ + [100, 50, 50, 50], + [.1, .05, .05, 50], + [12.50, 10, 2.5, 20], + [99.99, 84.99, 15.0, 15], + [9999999999.01, 8999999999.11, 999999999.9, 10], + [0, 0, 0, 0], + [0, 10, 0, 0], + [9.95, 9.95, 0, 0], + [21.05, 0, 21.05, 100] + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 485ae792193e3..1fe62fc442ecf 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -69,4 +69,6 @@ <type name="Magento\Framework\Search\Request\Config\FilesystemReader"> <plugin name="productAttributesDynamicFields" type="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader" /> </type> + + <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index fe3413dc3b218..f8a3e4ac5c895 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -118,4 +118,27 @@ </argument> </arguments> </type> + + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\AttributeProcessor"> + <arguments> + <argument name="fieldToAttributeMap" xsi:type="array"> + <item name="price_range" xsi:type="array"> + <item name="price" xsi:type="string">price</item> + </item> + </argument> + </arguments> + </type> + + <type name="Magento\Catalog\Block\Product\ImageFactory"> + <plugin name="designLoader" type="Magento\CatalogGraphQl\Plugin\DesignLoader" /> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 76a58857cebc2..536992d3fca82 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -11,26 +11,29 @@ type Query { ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( - id: Int @doc(description: "Id of the category.") + id: Int @doc(description: "Id of the category.") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + categoryList( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") } -type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") { - amount: Money @doc(description: "The price of a product plus a three-letter currency code.") - adjustments: [PriceAdjustment] @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") +type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") { + amount: Money @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "The price of a product plus a three-letter currency code.") + adjustments: [PriceAdjustment] @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") } -type PriceAdjustment @doc(description: "The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { +type PriceAdjustment @doc(description: "PriceAdjustment is deprecated. Taxes will be included or excluded in the price. The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { amount: Money @doc(description: "The amount of the price adjustment and its currency code.") - code: PriceAdjustmentCodesEnum @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") - description: PriceAdjustmentDescriptionEnum @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") + code: PriceAdjustmentCodesEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") + description: PriceAdjustmentDescriptionEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") } -enum PriceAdjustmentCodesEnum @doc(description: "Note: This enumeration contains values defined in modules other than the Catalog module.") { +enum PriceAdjustmentCodesEnum @doc(description: "PriceAdjustment.code is deprecated. This enumeration contains values defined in modules other than the Catalog module.") { } -enum PriceAdjustmentDescriptionEnum @doc(description: "This enumeration states whether a price adjustment is included or excluded.") { +enum PriceAdjustmentDescriptionEnum @doc(description: "PriceAdjustmentDescriptionEnum is deprecated. This enumeration states whether a price adjustment is included or excluded.") { INCLUDED EXCLUDED } @@ -41,10 +44,26 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") { DYNAMIC } -type ProductPrices @doc(description: "The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { - minimalPrice: Price @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") - maximalPrice: Price @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") - regularPrice: Price @doc(description: "The base price of a product.") +type ProductPrices @doc(description: "ProductPrices is deprecated, replaced by PriceRange. The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { + minimalPrice: Price @deprecated(reason: "Use PriceRange.minimum_price.") @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") + maximalPrice: Price @deprecated(reason: "Use PriceRange.maximum_price.") @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") + regularPrice: Price @deprecated(reason: "Use regular_price from PriceRange.minimum_price or PriceRange.maximum_price.") @doc(description: "The base price of a product.") +} + +type PriceRange @doc(description: "Price range for a product. If the product has a single price, the minimum and maximum price will be the same."){ + minimum_price: ProductPrice! @doc(description: "The lowest possible price for the product.") + maximum_price: ProductPrice @doc(description: "The highest possible price for the product.") +} + +type ProductPrice @doc(description: "Represents a product price.") { + regular_price: Money! @doc(description: "The regular price of the product.") + final_price: Money! @doc(description: "The final price of the product after discounts applied.") + discount: ProductDiscount @doc(description: "The price discount. Represents the difference between the regular and final price.") +} + +type ProductDiscount @doc(description: "A discount applied to a product price.") { + percent_off: Float @doc(description: "The discount expressed a percentage.") + amount_off: Float @doc(description: "The actual value of the discount.") } type ProductLinks implements ProductLinksInterface @doc(description: "ProductLinks is an implementation of ProductLinksInterface.") { @@ -58,14 +77,6 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M position: Int @doc(description: "The position within the list of product links.") } -type ProductTierPrices @doc(description: "The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") { - customer_group_id: String @doc(description: "The ID of the customer group.") - qty: Float @doc(description: "The number of items that must be purchased to qualify for tier pricing.") - value: Float @doc(description: "The price of the fixed price item.") - percentage_value: Float @doc(description: "The percentage discount of the item.") - website_id: Float @doc(description: "The ID assigned to the website.") -} - interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") name: String @doc(description: "The product name. Customers use this name to identify the product.") @@ -84,7 +95,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") - tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") + tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") created_at: String @doc(description: "Timestamp indicating when the product was created.") updated_at: String @doc(description: "Timestamp indicating when the product was updated.") @@ -93,8 +104,8 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ websites: [Website] @doc(description: "An array of websites in which the product is available.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks") media_gallery_entries: [MediaGalleryEntry] @deprecated(reason: "Use product's `media_gallery` instead") @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") - tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices") - price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") + price: ProductPrices @deprecated(reason: "Use price_range for product price information.") @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") + price_range: PriceRange! @doc(description: "A PriceRange object, indicating the range of prices for the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\PriceRange") gift_message_available: String @doc(description: "Indicates whether a gift message is available.") manufacturer: Int @doc(description: "A number representing the product's manufacturer.") categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") @@ -231,6 +242,7 @@ type Breadcrumb @doc(description: "Breadcrumb item."){ category_name: String @doc(description: "Category name.") category_level: Int @doc(description: "Category level.") category_url_key: String @doc(description: "Category URL key.") + category_url_path: String @doc(description: "Category URL path.") } type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option.") { @@ -285,6 +297,13 @@ input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") } +input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") +{ + ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") + url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category") + name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") +} + input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 428c61c7fec0f..5baa4b4274be5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -484,7 +484,9 @@ protected function initTypeModels() } if ($model->isSuitable()) { $this->_productTypeModels[$productTypeName] = $model; + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_disabledAttrs = array_merge($this->_disabledAttrs, $model->getDisabledAttrs()); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_indexValueAttributes = array_merge( $this->_indexValueAttributes, $model->getIndexValueAttributes() @@ -526,7 +528,7 @@ protected function getMediaGallery(array $productIds) if (empty($productIds)) { return []; } - + $productEntityJoinField = $this->getProductEntityLinkField(); $select = $this->_connection->select()->from( @@ -710,6 +712,21 @@ public function _getHeaderColumns() return $this->_customHeadersMapping($this->rowCustomizer->addHeaderColumns($this->_headerColumns)); } + /** + * Return non-system attributes + + * @return array + */ + private function getNonSystemAttributes(): array + { + $attrKeys = []; + foreach ($this->filterAttributeCollection($this->getAttributeCollection()) as $attribute) { + $attrKeys[] = $attribute->getAttributeCode(); + } + + return array_diff($this->_getExportMainAttrCodes(), $this->_customHeadersMapping($attrKeys)); + } + /** * Set headers columns * @@ -722,6 +739,18 @@ public function _getHeaderColumns() */ protected function setHeaderColumns($customOptionsData, $stockItemRows) { + $exportAttributes = ( + array_key_exists("skip_attr", $this->_parameters) && count($this->_parameters["skip_attr"]) + ) ? + array_intersect( + $this->_getExportMainAttrCodes(), + array_merge( + $this->_customHeadersMapping($this->_getExportAttrCodes()), + $this->getNonSystemAttributes() + ) + ) : + $this->_getExportMainAttrCodes(); + if (!$this->_headerColumns) { $this->_headerColumns = array_merge( [ @@ -732,7 +761,7 @@ protected function setHeaderColumns($customOptionsData, $stockItemRows) self::COL_CATEGORY, self::COL_PRODUCT_WEBSITES, ], - $this->_getExportMainAttrCodes(), + $exportAttributes, [self::COL_ADDITIONAL_ATTRIBUTES], reset($stockItemRows) ? array_keys(end($stockItemRows)) : [], [ @@ -923,6 +952,7 @@ protected function getExportData() foreach ($rawData as $productId => $productData) { foreach ($productData as $storeId => $dataRow) { if ($storeId == Store::DEFAULT_STORE_ID && isset($stockItemRows[$productId])) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $dataRow = array_merge($dataRow, $stockItemRows[$productId]); } $this->appendMultirowData($dataRow, $multirawData); @@ -1330,7 +1360,7 @@ private function appendMultirowData(&$dataRow, $multiRawData) $dataRow[self::COL_SKU] = $sku; $dataRow[self::COL_ATTR_SET] = $attributeSet; $dataRow[self::COL_TYPE] = $type; - + return $dataRow; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 4ff995c2a872c..1ae993ed99060 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -141,7 +141,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity const COL_PRODUCT_WEBSITES = '_product_websites'; /** - * Media gallery attribute code. + * Attribute code for media gallery. */ const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery'; @@ -151,12 +151,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity const COL_MEDIA_IMAGE = '_media_image'; /** - * Inventory use config. + * Inventory use config label. */ const INVENTORY_USE_CONFIG = 'Use Config'; /** - * Inventory use config prefix. + * Prefix for inventory use config. */ const INVENTORY_USE_CONFIG_PREFIX = 'use_config_'; @@ -1886,6 +1886,7 @@ protected function _saveProducts() return $this; } + //phpcs:enable Generic.Metrics.NestingLevel /** * Prepare array with image states (visible or hidden from product page) @@ -2736,8 +2737,6 @@ protected function _saveValidatedBunches() try { $rowData = $source->current(); } catch (\InvalidArgumentException $e) { - $this->addRowError($e->getMessage(), $this->_processedRowsCount); - $this->_processedRowsCount++; $source->next(); continue; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 3b6caef66ce6c..d87c3d8477556 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -13,6 +13,7 @@ /** * Import entity abstract product type model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.TooManyFields) @@ -543,7 +544,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe } else { $resultAttrs[$attrCode] = $rowData[$attrCode]; } - } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) { + } elseif (array_key_exists($attrCode, $rowData)) { $resultAttrs[$attrCode] = $rowData[$attrCode]; } elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $attrParams['default_value']; diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index bd2fe896b8c0a..371d75bc922f3 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -72,7 +72,9 @@ protected function setUp() 'setAttributeSetFilter' ] ); - $attribute = $this->createPartialMock(\Magento\Eav\Model\Entity\Attribute::class, [ + $attribute = $this->createPartialMock( + \Magento\Eav\Model\Entity\Attribute::class, + [ 'getAttributeCode', 'getId', 'getIsVisible', @@ -85,7 +87,8 @@ protected function setUp() 'getDefaultValue', 'usesSource', 'getFrontendInput', - ]); + ] + ); $attribute->expects($this->any())->method('getIsVisible')->willReturn(true); $attribute->expects($this->any())->method('getIsGlobal')->willReturn(true); $attribute->expects($this->any())->method('getIsRequired')->willReturn(true); @@ -107,6 +110,7 @@ protected function setUp() ]; $attribute1 = clone $attribute; $attribute2 = clone $attribute; + $attribute3 = clone $attribute; $attribute1->expects($this->any())->method('getId')->willReturn('1'); $attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code'); @@ -118,6 +122,11 @@ protected function setUp() $attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean'); $attribute2->expects($this->any())->method('isStatic')->willReturn(false); + $attribute3->expects($this->any())->method('getId')->willReturn('3'); + $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute'); + $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text'); + $attribute3->expects($this->any())->method('isStatic')->willReturn(false); + $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls( ['option1', 'option2'], @@ -126,7 +135,9 @@ protected function setUp() $attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection); $attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]); $attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection); - $attrCollection->expects($this->any())->method('setAttributeSetFilter')->willReturn([$attribute1, $attribute2]); + $attrCollection->expects($this->any()) + ->method('setAttributeSetFilter') + ->willReturn([$attribute1, $attribute2, $attribute3]); $attributeSet->expects($this->any())->method('getId')->willReturn(1); $attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name'); @@ -157,9 +168,11 @@ protected function setUp() ], ] ) - ->willReturn([$attribute1, $attribute2]); + ->willReturn([$attribute1, $attribute2, $attribute3]); - $this->connection = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, [ + $this->connection = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ 'select', 'fetchAll', 'fetchPairs', @@ -167,13 +180,17 @@ protected function setUp() 'insertOnDuplicate', 'delete', 'quoteInto' - ]); - $this->select = $this->createPartialMock(\Magento\Framework\DB\Select::class, [ + ] + ); + $this->select = $this->createPartialMock( + \Magento\Framework\DB\Select::class, + [ 'from', 'where', 'joinLeft', 'getConnection', - ]); + ] + ); $this->select->expects($this->any())->method('from')->will($this->returnSelf()); $this->select->expects($this->any())->method('where')->will($this->returnSelf()); $this->select->expects($this->any())->method('joinLeft')->will($this->returnSelf()); @@ -189,10 +206,13 @@ protected function setUp() ->method('fetchAll') ->will($this->returnValue($entityAttributes)); - $this->resource = $this->createPartialMock(\Magento\Framework\App\ResourceConnection::class, [ + $this->resource = $this->createPartialMock( + \Magento\Framework\App\ResourceConnection::class, + [ 'getConnection', 'getTableName', - ]); + ] + ); $this->resource->expects($this->any())->method('getConnection')->will( $this->returnValue($this->connection) ); @@ -257,9 +277,13 @@ public function testIsRowValidSuccess() $rowNum = 1; $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null); $this->entityModel->expects($this->never())->method('addRowError'); - $this->setPropertyValue($this->simpleType, '_attributes', [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [], - ]); + $this->setPropertyValue( + $this->simpleType, + '_attributes', + [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [], + ] + ); $this->assertTrue($this->simpleType->isRowValid($rowData, $rowNum)); } @@ -278,13 +302,17 @@ public function testIsRowValidError() 'attr_code' ) ->willReturnSelf(); - $this->setPropertyValue($this->simpleType, '_attributes', [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [ - 'attr_code' => [ - 'is_required' => true, + $this->setPropertyValue( + $this->simpleType, + '_attributes', + [ + $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [ + 'attr_code' => [ + 'is_required' => true, + ], ], - ], - ]); + ] + ); $this->assertFalse($this->simpleType->isRowValid($rowData, $rowNum)); } @@ -364,9 +392,14 @@ public function testPrepareAttributesWithDefaultValueForSave() { $rowData = [ '_attribute_set' => 'attributeSetName', - 'boolean_attribute' => 'Yes' + 'boolean_attribute' => 'Yes', + ]; + + $expected = [ + 'boolean_attribute' => 1, + 'text_attribute' => 'default_value' ]; $result = $this->simpleType->prepareAttributesWithDefaultValueForSave($rowData); - $this->assertEquals(['boolean_attribute' => 1], $result); + $this->assertEquals($expected, $result); } } diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index f9a49d4f8d121..35231b8460b19 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -104,6 +104,10 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = $select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1'); } + if (!empty($entityIds)) { + $select->where('stock_item.product_id in (?)', $entityIds); + } + $select->group('stock_item.product_id'); $select->having('max_is_in_stock = 0'); diff --git a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index 7f43cd279d4e3..0000000000000 --- a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogInventory\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tableName = $this->schemaSetup->getTable('cataloginventory_stock_status_tmp'); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminAssertDisabledQtyActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminAssertDisabledQtyActionGroup.xml new file mode 100644 index 0000000000000..27c4a93577a07 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminAssertDisabledQtyActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertDisabledQtyActionGroup"> + <annotations> + <description>Goes to the 'Quantity' field and assert disabled attribute.</description> + </annotations> + + <seeElement selector="{{AdminProductFormSection.productQuantity}}" stepKey="assertProductQty"/> + <assertElementContainsAttribute selector="{{AdminProductFormSection.productQuantity}}" attribute="disabled" expectedValue="true" stepKey="checkIfQtyIsDisabled" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php new file mode 100644 index 0000000000000..46f4e0f26f378 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogInventory\Test\Unit\Model\Indexer; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Indexer\ProductPriceIndexFilter; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; + +/** + * Product Price filter test, to ensure that product id's filtered. + */ +class ProductPriceIndexFilterTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var MockObject|StockConfigurationInterface $stockConfiguration + */ + private $stockConfiguration; + + /** + * @var MockObject|Item $item + */ + private $item; + + /** + * @var MockObject|ResourceConnection $resourceCnnection + */ + private $resourceCnnection; + + /** + * @var MockObject|Generator $generator + */ + private $generator; + + /** + * @var ProductPriceIndexFilter $productPriceIndexFilter + */ + private $productPriceIndexFilter; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->item = $this->createMock(Item::class); + $this->resourceCnnection = $this->createMock(ResourceConnection::class); + $this->generator = $this->createMock(Generator::class); + + $this->productPriceIndexFilter = new ProductPriceIndexFilter( + $this->stockConfiguration, + $this->item, + $this->resourceCnnection, + 'indexer', + $this->generator, + 100 + ); + } + + /** + * Test to ensure that Modify Price method uses entityIds, + */ + public function testModifyPrice() + { + $entityIds = [1, 2, 3]; + $indexTableStructure = $this->createMock(IndexTableStructure::class); + $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $this->resourceCnnection->expects($this->once())->method('getConnection')->willReturn($connectionMock); + $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); + $connectionMock->expects($this->once())->method('select')->willReturn($selectMock); + $selectMock->expects($this->at(2)) + ->method('where') + ->with('stock_item.product_id in (?)', $entityIds) + ->willReturn($selectMock); + $this->generator->expects($this->once()) + ->method('generate') + ->will( + $this->returnCallback( + $this->getBatchIteratorCallback($selectMock, 5) + ) + ); + + $fetchStmtMock = $this->createPartialMock(\Zend_Db_Statement_Pdo::class, ['fetchAll']); + $fetchStmtMock->expects($this->any()) + ->method('fetchAll') + ->will($this->returnValue([['product_id' => 1]])); + $connectionMock->expects($this->any())->method('query')->will($this->returnValue($fetchStmtMock)); + $this->productPriceIndexFilter->modifyPrice($indexTableStructure, $entityIds); + } + + /** + * Returns batches. + * + * @param MockObject $selectMock + * @param int $batchCount + * @return \Closure + */ + private function getBatchIteratorCallback(MockObject $selectMock, int $batchCount): \Closure + { + $iteratorCallback = function () use ($batchCount, $selectMock): array { + $result = []; + $count = $batchCount; + while ($count) { + $count--; + $result[$count] = $selectMock; + } + + return $result; + }; + + return $iteratorCallback; + } +} 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 da465f5bdd3dc..789befcfec8b7 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 @@ -1,11 +1,10 @@ <?php - -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; @@ -238,7 +237,7 @@ private function prepareMeta() $this->meta = $this->arrayManager->merge( $fieldsetPath . '/children', $this->meta, - ['container_quantity_and_stock_status_qty' => $container] + ['quantity_and_stock_status_qty' => $container] ); } } diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml index 5ac7fedc5aa18..b5c4a96f24a94 100644 --- a/app/code/Magento/CatalogInventory/etc/db_schema.xml +++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="cataloginventory_stock" resource="default" engine="innodb" comment="Cataloginventory Stock"> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="varchar" name="stock_name" nullable="true" length="255" comment="Stock Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="stock_id"/> @@ -22,11 +22,11 @@ </table> <table name="cataloginventory_stock_item" resource="default" engine="innodb" comment="Cataloginventory Stock Item"> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Stock Id"/> + default="0" comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="decimal" name="min_qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Min Qty"/> @@ -94,11 +94,11 @@ <table name="cataloginventory_stock_status" resource="default" engine="innodb" comment="Cataloginventory Stock Status"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -121,11 +121,11 @@ <table name="cataloginventory_stock_status_idx" resource="default" engine="innodb" comment="Cataloginventory Stock Status Indexer Idx"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -142,14 +142,14 @@ <column name="website_id"/> </index> </table> - <table name="cataloginventory_stock_status_tmp" resource="default" engine="memory" + <table name="cataloginventory_stock_status_tmp" resource="default" engine="innodb" comment="Cataloginventory Stock Status Indexer Tmp"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" @@ -159,21 +159,21 @@ <column name="website_id"/> <column name="stock_id"/> </constraint> - <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> - <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="cataloginventory_stock_status_replica" resource="default" engine="innodb" comment="Cataloginventory Stock Status"> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml index 00dcb68089b73..209095e0b0195 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml @@ -21,7 +21,6 @@ <waitForPageLoad stepKey="waitForPageToLoad"/> <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> - <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{catalogRule.is_active}}" stepKey="selectStatus"/> <selectOption stepKey="selectWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml index 77fe0f50653c7..0a4b6366d11a8 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml @@ -13,7 +13,7 @@ <description>Validates that the provided Catalog Rule, Status, Websites and Customer Group details are present and correct on a Admin Catalog Price Rule creation/edit page.</description> </annotations> <arguments> - <argument name="catalogRule" defaultValue="inactiveCatalogRule"/> + <argument name="catalogRule" defaultValue="inactiveCatalogRule" /> <argument name="status" type="string" defaultValue=""/> <argument name="websites" type="string"/> <argument name="customerGroup" type="string"/> @@ -21,7 +21,6 @@ <seeInField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <seeInField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> - <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.status}}" userInput="{{status}}" stepKey="selectStatus"/> <see stepKey="seeWebSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{websites}}"/> <seeOptionIsSelected selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup"/> <scrollTo selector="{{AdminNewCatalogPriceRule.actionsTab}}" stepKey="scrollToActionTab"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index a7500858fc94e..09053b5ad14a3 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -25,6 +25,7 @@ <!-- Fill the form according the attributes of the entity --> <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption stepKey="selectSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> <click stepKey="clickFromCalender" selector="{{AdminNewCatalogPriceRule.fromDateButton}}"/> <click stepKey="clickFromToday" selector="{{AdminNewCatalogPriceRule.todayDate}}"/> @@ -47,17 +48,39 @@ <arguments> <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> </arguments> - <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> - <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName"/> - <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription"/> - <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite"/> + <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> + <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> <waitForPageLoad stepKey="waitForApplied"/> </actionGroup> + <actionGroup name="AdminCreateCatalogPriceRuleWithConditionActionGroup" extends="createCatalogPriceRule"> + <arguments> + <argument name="catalogRuleType" type="entity" defaultValue="PriceRuleWithCondition"/> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad" after="addNewRule"/> + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="expandConditions" before="openActionDropdown"/> + <scrollTo selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="scrollToConditionsTab" after="expandConditions"/> + <waitForElementVisible selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="waitForNewRule" after="scrollToConditionsTab"/> + <click selector="{{PriceRuleConditionsSection.createNewRule}}" stepKey="clickNewRule" after="waitForNewRule"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionsDropdown}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="selectProductAttribute" after="clickNewRule"/> + <waitForPageLoad stepKey="waitForAttributeLoad" after="selectProductAttribute"/> + <!--Assert that attribute contains today date without time--> + <comment userInput="Assert that attribute contains today date without time" stepKey="assertDate" after="waitForAttributeLoad"/> + <generateDate date="now" format="Y-m-d" stepKey="today" after="assertDate"/> + <grabTextFrom selector="{{PriceRuleConditionsSection.firstProductAttributeSelected}}" stepKey="grabTextFromSelectedAttribute" after="today"/> + <assertEquals expected="$today" actual="$grabTextFromSelectedAttribute" stepKey="assertTodayDate" after="grabTextFromSelectedAttribute"/> + </actionGroup> + <actionGroup name="AdminCreateMultipleWebsiteCatalogPriceRule" extends="createCatalogPriceRule"> + <remove keyForRemoval="selectSite"/> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="['FirstWebsite', 'SecondWebsite']" stepKey="selectWebsite"/> + </actionGroup> <actionGroup name="CreateCatalogPriceRuleViaTheUi"> <arguments> <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml index bab9842caaa42..7a92829e2371e 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleStagingSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCatalogPriceRuleStagingSection"> <element name="status" type="select" selector=".modal-component [data-index='is_active'] select"/> + <element name="isActive" type="select" selector=".modals-wrapper input[name='is_active']+label"/> </section> </sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml index ba0493d8e995b..c736dd8dde2cb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -20,8 +20,12 @@ <element name="ruleNameNew" type="input" selector="[name='staging[name]']"/> <element name="description" type="textarea" selector="[name='description']"/> <element name="status" type="select" selector="[name='is_active']"/> + <element name="isActive" type="select" selector="input[name='is_active']+label"/> <element name="websites" type="select" selector="[name='website_ids']"/> + <element name="active" type="checkbox" selector="//div[contains(@class, 'admin__actions-switch')]/input[@name='is_active']/../label"/> + <element name="activeIsEnabled" type="checkbox" selector="(//div[contains(@class, 'admin__actions-switch')])[1]/input[@value='1']"/> + <element name="activePosition" type="checkbox" selector="fieldset[class='admin__fieldset'] div[class*='_required']:nth-of-type(4)"/> <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="select" selector="[name='customer_group_ids']"/> <element name="customerGroupsOptions" type="select" selector="[name='customer_group_ids'] option"/> @@ -43,6 +47,7 @@ <section name="AdminNewCatalogPriceRuleConditions"> <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> + <element name="conditionsDropdown" type="select" selector="select[data-form-part='catalog_rule_form'][data-ui-id='newchild-0-select-rule-conditions-1-new-child']"/> <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml index 741da96179b8c..ca534ec7f5375 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml @@ -52,6 +52,7 @@ <waitForPageLoad stepKey="waitForIndividualRulePage"/> <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{_defaultCatalogRule.name}}" stepKey="fillName"/> <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{_defaultCatalogRule.description}}" stepKey="fillDescription"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{_defaultCatalogRule.website_ids[0]}}" stepKey="selectSite"/> <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml index befe0b0ce7f98..09b924603c54a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml @@ -25,6 +25,10 @@ <requiredEntity createDataKey="createCategory"/> </createData> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- log in and create the price rule --> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> @@ -150,6 +154,7 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> @@ -172,6 +177,10 @@ <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory1"/> <see selector="{{StorefrontCategoryProductSection.ProductInfoByNumber('1')}}" userInput="$$createProduct.name$$" stepKey="seeProduct1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml index 5223b18df4e4a..83dff1ecdcab5 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml @@ -58,6 +58,7 @@ <argument name="websites" value="Main Website"/> <argument name="customerGroup" value="General"/> </actionGroup> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveRule"/> <!-- Search Catalog Rule in Grid --> <actionGroup ref="AdminSearchCatalogRuleInGridActionGroup" stepKey="searchCreatedCatalogRule"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml index 06392764290ac..ea5c2c33a0a39 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml @@ -154,6 +154,10 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml index b7a231df5045d..dd54c0767e8e1 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml @@ -195,6 +195,7 @@ Websites: Main Website Customer Groups: NOT LOGGED IN --> <fillField userInput="{{SimpleCatalogPriceRule.name}}" selector="{{AdminCartPriceRulesFormSection.ruleName}}" stepKey="fillRuleName"/> + <click stepKey="selectActive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <selectOption userInput="{{SimpleCatalogPriceRule.websites}}" selector="{{AdminCartPriceRulesFormSection.websites}}" stepKey="selectWebsite"/> <selectOption userInput="{{SimpleCatalogPriceRule.customerGroups}}" selector="{{AdminCartPriceRulesFormSection.customerGroups}}" stepKey="selectCustomerGroups"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 5b7e722c92a02..a251ee1e235d0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -77,63 +77,27 @@ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> - + <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> - - <!-- Check product 1 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> - <argument name="productInfo" value="$createProduct1.name$"/> - <argument name="productNumber" value="3"/> - </actionGroup> <!-- Check product 1 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="3"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$51.10" stepKey="storefrontProduct1Price"/> <!-- Check product 1 regular price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="3"/> - </actionGroup> - - <!-- Check product 2 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Name"> - <argument name="productInfo" value="$createProduct2.name$"/> - <argument name="productNumber" value="2"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$56.78" stepKey="storefrontProduct1RegularPrice"/> <!-- Check product 2 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="2"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$51.10" stepKey="storefrontProduct2Price"/> - <!-- Check product 2 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct2RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="2"/> - </actionGroup> - - <!-- Check product 3 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Name"> - <argument name="productInfo" value="$createProduct3.name$"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <!-- Check product 2 regular price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$56.78" stepKey="storefrontProduct2RegularPrice"/> <!-- Check product 3 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3Price"> - <argument name="productInfo" value="$51.10"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$51.10" stepKey="storefrontProduct3Price"/> <!-- Check product 3 regular price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct3RegularPrice"> - <argument name="productInfo" value="$56.78"/> - <argument name="productNumber" value="1"/> - </actionGroup> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$56.78" stepKey="storefrontProduct3RegularPrice"/> <!-- Navigate to product 1 on store front --> <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index b486654fe9acf..08e59c6316411 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -26,9 +26,14 @@ </createData> <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> <actionGroup stepKey="selectLoggedInCustomers" ref="selectNotLoggedInCustomerGroup"/> - <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="Inactive" stepKey="setInactive"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="setInactive" selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}"/> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="seeSuccess"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml index 59082e93b04c2..b3692f280fec5 100644 --- a/app/code/Magento/CatalogRule/etc/db_schema.xml +++ b/app/code/Magento/CatalogRule/etc/db_schema.xml @@ -37,7 +37,7 @@ </table> <table name="catalogrule_product" resource="default" engine="innodb" comment="CatalogRule Product"> <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> + comment="Rule Product ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" @@ -46,7 +46,7 @@ comment="To time"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="varchar" name="action_operator" nullable="true" length="10" default="to_fixed" comment="Action Operator"/> <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" @@ -56,7 +56,7 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> @@ -91,11 +91,11 @@ <column xsi:type="date" name="rule_date" nullable="false" comment="Rule Date"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Rule Price"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -121,9 +121,9 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> + default="0" comment="Customer Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> @@ -140,7 +140,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -160,7 +160,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> @@ -177,7 +177,7 @@ </table> <table name="catalogrule_product_replica" resource="default" engine="innodb" comment="CatalogRule Product"> <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Product Id"/> + comment="Rule Product ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" @@ -186,7 +186,7 @@ comment="To time"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="varchar" name="action_operator" nullable="true" default="to_fixed" length="10" comment="Action Operator"/> <column xsi:type="decimal" name="action_amount" scale="6" precision="20" unsigned="false" nullable="false" @@ -196,7 +196,7 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> @@ -232,11 +232,11 @@ <column xsi:type="date" name="rule_date" nullable="false" comment="Rule Date"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Product Id"/> + default="0" comment="Product ID"/> <column xsi:type="decimal" name="rule_price" scale="6" precision="20" unsigned="false" nullable="false" default="0" comment="Rule Price"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -263,9 +263,9 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Group Id"/> + default="0" comment="Customer Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml index c114f6b1d77cd..2af8bb0770b20 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml +++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml @@ -95,33 +95,30 @@ <dataScope>description</dataScope> </settings> </field> - <field name="is_active" formElement="select"> + <field name="is_active" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">catalog_rule</item> + <item name="default" xsi:type="number">0</item> </item> </argument> <settings> - <dataType>number</dataType> - <label translate="true">Status</label> - <visible>true</visible> - <dataScope>is_active</dataScope> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + <dataType>boolean</dataType> + <label translate="true">Active</label> </settings> <formElements> - <select> + <checkbox> <settings> - <options> - <option name="0" xsi:type="array"> - <item name="value" xsi:type="number">1</item> - <item name="label" xsi:type="string" translate="true">Active</item> - </option> - <option name="1" xsi:type="array"> - <item name="value" xsi:type="number">0</item> - <item name="label" xsi:type="string" translate="true">Inactive</item> - </option> - </options> + <valueMap> + <map name="false" xsi:type="number">0</map> + <map name="true" xsi:type="number">1</map> + </valueMap> + <prefer>toggle</prefer> </settings> - </select> + </checkbox> </formElements> </field> <field name="website_ids" formElement="multiselect"> diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e9fb1070fedd5..3b0c4dfb6df2f 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\Catalog\Model\Layer\Filter\AbstractFilter; @@ -12,6 +14,9 @@ */ class Decimal extends AbstractFilter { + /** Decimal delta for filter */ + private const DECIMAL_DELTA = 0.001; + /** * @var \Magento\Framework\Pricing\PriceCurrencyInterface */ @@ -70,11 +75,17 @@ public function apply(\Magento\Framework\App\RequestInterface $request) list($from, $to) = explode('-', $filter); + // When the range is 10-20 we only need to get products that are in the 10-19.99 range. + $toValue = $to; + if (!empty($toValue) && $from !== $toValue) { + $toValue -= self::DECIMAL_DELTA; + } + $this->getLayer() ->getProductCollection() ->addFieldToFilter( $this->getAttributeModel()->getAttributeCode(), - ['from' => $from, 'to' => $to] + ['from' => $from, 'to' => $toValue] ); $this->getLayer()->getState()->addFilter( @@ -111,7 +122,7 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = null; + $to = ''; } $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; @@ -138,7 +149,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === null) { + if ($toPrice === '') { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index a19f53469ae01..66d9281ed38e2 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\Catalog\Model\Layer\Filter\AbstractFilter; @@ -11,6 +13,7 @@ * Layer price filter based on Search API * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Price extends AbstractFilter { @@ -138,7 +141,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) list($from, $to) = $filter; $this->getLayer()->getProductCollection()->addFieldToFilter( - 'price', + $this->getAttributeModel()->getAttributeCode(), ['from' => $from, 'to' => empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA] ); diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 595bc12ca956a..edc65490a3c67 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -393,6 +393,7 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se /** This variable sets by serOrder method, but doesn't have a getter method. */ 'orders' => $this->_orders, 'size' => $this->getPageSize(), + 'currentPage' => (int)$this->_curPage, ] ); } diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 8f8ba39ebd329..5ac252677ff79 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Search; use Magento\Catalog\Api\Data\EavAttributeInterface; @@ -78,6 +80,7 @@ private function generateRequest($attributeType, $container, $useFulltext) { $request = []; foreach ($this->getSearchableAttributes() as $attribute) { + /** @var $attribute Attribute */ if ($attribute->getData($attributeType)) { if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) { $queryName = $attribute->getAttributeCode() . '_query'; @@ -97,12 +100,14 @@ private function generateRequest($attributeType, $container, $useFulltext) ], ]; $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX; - $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType()); + $generatorType = $attribute->getFrontendInput() === 'price' + ? $attribute->getFrontendInput() + : $attribute->getBackendType(); + $generator = $this->generatorResolver->getGeneratorForType($generatorType); $request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName); $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName); } } - /** @var $attribute Attribute */ if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price'], true)) { // Some fields have their own specific handlers continue; diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php index b3d39a48fe9fc..73d011cc532db 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Model\Search\RequestGenerator; diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php new file mode 100644 index 0000000000000..949806d14f45a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Model\Search\RequestGenerator; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Search\Request\FilterInterface; + +/** + * Catalog search range request generator. + */ +class Price implements GeneratorInterface +{ + /** + * @inheritdoc + */ + public function getFilterData(Attribute $attribute, $filterName): array + { + return [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attribute->getAttributeCode(), + 'from' => '$' . $attribute->getAttributeCode() . '.from$', + 'to' => '$' . $attribute->getAttributeCode() . '.to$', + ]; + } + + /** + * @inheritdoc + */ + public function getAggregationData(Attribute $attribute, $bucketName): array + { + return [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attribute->getAttributeCode(), + 'method' => '$price_dynamic_algorithm$', + 'metric' => [['type' => 'count']], + ]; + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml new file mode 100644 index 0000000000000..1afdb6e5e46fa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillFormAdvancedSearchActionGroup"> + <arguments> + <argument name="productName" type="string" defaultValue=""/> + <argument name="sku" type="string" defaultValue=""/> + <argument name="description" type="string" defaultValue=""/> + <argument name="short_description" type="string" defaultValue=""/> + <argument name="price_from" type="string" defaultValue=""/> + <argument name="price_to" type="string" defaultValue=""/> + </arguments> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.ProductName}}" userInput="{{productName}}" stepKey="fillName"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.SKU}}" userInput="{{sku}}" stepKey="fillSku"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.Description}}" userInput="{{description}}" stepKey="fillDescription"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.ShortDescription}}" userInput="{{short_description}}" stepKey="fillShortDescription"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceFrom}}" userInput="{{price_from}}" stepKey="fillPriceFrom"/> + <fillField selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceTo}}" userInput="{{price_to}}" stepKey="fillPriceTo"/> + <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml index 6b28b4f36c6a7..eb3bc8e79d7b5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml @@ -17,5 +17,6 @@ <element name="message" type="text" selector="div.message div"/> <element name="itemFound" type="text" selector=".search.found>strong"/> <element name="productName" type="text" selector=".product.name.product-item-name>a"/> + <element name="nthProductName" type="text" selector="li.product-item:nth-of-type({{var1}}) .product-item-name>a" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml index 13665100f79af..0e92d9fb0c7ad 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml @@ -13,6 +13,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> <argument name="name" value="$$product.name$$"/> @@ -26,6 +31,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> <argument name="sku" value="$$product.sku$$"/> @@ -39,6 +49,10 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> <argument name="description" value="$$product.custom_attributes[description]$$"/> @@ -52,6 +66,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> <argument name="shortDescription" value="$$product.custom_attributes[short_description]$$"/> @@ -65,6 +84,11 @@ <features value="CatalogSearch"/> <group value="CatalogSearch"/> </annotations> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> <argument name="name" value="$$arg1.name$$"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 99f3fc00a7401..aa7cf933f6328 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -9,6 +9,72 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CGuestUserTest"> + <!-- Step 2: User searches for product --> + <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> + <!-- Advanced Search with Product Data --> + <comment userInput="Advanced search" stepKey="commentAdvancedSearch" after="startOfSearchingProducts"/> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="searchOpenAdvancedSearchForm" after="commentAdvancedSearch"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <fillField userInput="$$createSimpleProduct1.name$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.ProductName}}" stepKey="searchAdvancedFillProductName" after="searchOpenAdvancedSearchForm"/> + <fillField userInput="$$createSimpleProduct1.sku$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.SKU}}" stepKey="searchAdvancedFillSKU" after="searchAdvancedFillProductName"/> + <fillField userInput="$$createSimpleProduct1.price$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceFrom}}" stepKey="searchAdvancedFillPriceFrom" after="searchAdvancedFillSKU"/> + <fillField userInput="$$createSimpleProduct1.price$$" selector="{{StorefrontCatalogSearchAdvancedFormSection.PriceTo}}" stepKey="searchAdvancedFillPriceTo" after="searchAdvancedFillPriceFrom"/> + <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="searchClickAdvancedSearchSubmitButton" after="searchAdvancedFillPriceTo"/> + <waitForLoadingMaskToDisappear stepKey="waitForSearchProductsloaded" after="searchClickAdvancedSearchSubmitButton"/> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="searchCheckAdvancedSearchResult" after="waitForSearchProductsloaded"/> + <see userInput="4" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productCount}} span" stepKey="searchAdvancedAssertProductCount" after="searchCheckAdvancedSearchResult"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertSimpleProduct1" after="searchAdvancedAssertProductCount"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="searchAdvancedGrabSimpleProduct1ImageSrc" after="searchAssertSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchAdvancedGrabSimpleProduct1ImageSrc" stepKey="searchAdvancedAssertSimpleProduct1ImageNotDefault" after="searchAdvancedGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="searchClickSimpleProduct1View" after="searchAdvancedAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForSearchSimpleProduct1Viewloaded" after="searchClickSimpleProduct1View"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="searchAssertSimpleProduct1Page" after="waitForSearchSimpleProduct1Viewloaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchAdvancedGrabSimpleProduct1PageImageSrc" after="searchAssertSimpleProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchAdvancedGrabSimpleProduct1PageImageSrc" stepKey="searchAdvancedAssertSimpleProduct1PageImageNotDefault" after="searchAdvancedGrabSimpleProduct1PageImageSrc"/> + + <!-- Quick Search with common part of product names --> + <comment userInput="Quick search" stepKey="commentQuickSearch" after="searchAdvancedAssertSimpleProduct1PageImageNotDefault"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchQuickSearchCommonPart" after="commentQuickSearch"> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="phrase" value="CONST.apiSimpleProduct"/> + </actionGroup> + <actionGroup ref="StorefrontSelectSearchFilterCategoryActionGroup" stepKey="searchSelectFilterCategoryCommonPart" after="searchQuickSearchCommonPart"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <see userInput="3" selector="{{StorefrontCategoryMainSection.productCount}} span" stepKey="searchAssertFilterCategoryProductCountCommonPart" after="searchSelectFilterCategoryCommonPart"/> + + <!-- Search simple product 1 --> + <comment userInput="Search simple product 1" stepKey="commentSearchSimpleProduct1" after="searchAssertFilterCategoryProductCountCommonPart"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertFilterCategorySimpleProduct1" after="commentSearchSimpleProduct1"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="searchGrabSimpleProduct1ImageSrc" after="searchAssertFilterCategorySimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabSimpleProduct1ImageSrc" stepKey="searchAssertSimpleProduct1ImageNotDefault" after="searchGrabSimpleProduct1ImageSrc"/> + <!-- Search simple product2 --> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="searchAssertFilterCategorySimpleProduct2" after="searchAssertSimpleProduct1ImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="searchGrabSimpleProduct2ImageSrc" after="searchAssertFilterCategorySimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabSimpleProduct2ImageSrc" stepKey="searchAssertSimpleProduct2ImageNotDefault" after="searchGrabSimpleProduct2ImageSrc"/> + + <!-- Quick Search with non-existent product name --> + <comment userInput="Quick Search with non-existent product name" stepKey="commentQuickSearchWithNonExistentProductName" after="searchAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchFillQuickSearchNonExistent" after="commentQuickSearchWithNonExistentProductName"> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="phrase" value="CONST.nonexistentProductName"/> + </actionGroup> + <see userInput="Your search returned no results." selector="{{StorefrontCatalogSearchMainSection.message}}" stepKey="searchAssertQuickSearchMessageNonExistent" after="searchFillQuickSearchNonExistent"/> + <comment userInput="End of searching products" stepKey="endOfSearchingProducts" after="searchAssertQuickSearchMessageNonExistent" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> <!-- Step 2: User searches for product --> <comment userInput="Start of searching products" stepKey="startOfSearchingProducts" after="endOfBrowsingCatalog"/> <!-- Advanced Search with Product 1 Data --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml new file mode 100644 index 0000000000000..c8f84c732d6ba --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="LayerNavigationOfCatalogSearchTest"> + <annotations> + <stories value="Search terms"/> + <title value="Layer Navigation of Catalog Search Should Equalize Price Range As Default Configuration"/> + <description value="Make sure filter of custom attribute with type of price displays on storefront Catalog page and price range should respect the configuration in Admin site"/> + <testCaseId value="MC-16979"/> + <useCaseId value="MC-16650"/> + <severity value="MAJOR"/> + <group value="CatalogSearch"/> + </annotations> + <before> + <magentoCLI command="config:set catalog/layered_navigation/price_range_calculation auto" stepKey="setAutoPriceRange"/> + <createData stepKey="createPriceAttribute" entity="productAttributeTypeOfPrice"/> + <createData stepKey="assignPriceAttributeGroup" entity="AddToDefaultSet"> + <requiredEntity createDataKey="createPriceAttribute"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="subCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="subCategory"/> + <deleteData stepKey="deleteSimpleProduct1" createDataKey="simpleProduct1"/> + <deleteData stepKey="deleteSimpleProduct2" createDataKey="simpleProduct2"/> + <deleteData createDataKey="createPriceAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Update value for price attribute of Product 1--> + <comment userInput="Update value for price attribute of Product 1" stepKey="comment1"/> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="navigateToCreatedProductEditPage1"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminProductFormSection.attributeLabelByText($$createPriceAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="grabAttributeLabel"/> + <fillField selector="{{AdminProductAttributeSection.customAttribute($$createPriceAttribute.attribute_code$$)}}" userInput="30" stepKey="fillCustomPrice1"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton1"/> + <waitForPageLoad stepKey="waitForSimpleProductSaved1"/> + <!--Update value for price attribute of Product 2--> + <comment userInput="Update value for price attribute of Product 1" stepKey="comment2"/> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="navigateToCreatedProductEditPage2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <fillField selector="{{AdminProductAttributeSection.customAttribute($$createPriceAttribute.attribute_code$$)}}" userInput="70" stepKey="fillCustomPrice2"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton2"/> + <waitForPageLoad stepKey="waitForSimpleProductSaved2"/> + + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <!--Navigate to category on Storefront--> + <comment userInput="Navigate to category on Storefront" stepKey="comment3"/> + <amOnPage url="{{StorefrontCategoryPage.url($$subCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="{$grabAttributeLabel}" selector="{{StorefrontCategoryFilterSection.CustomPriceAttribute}}" stepKey="seePriceLayerNavigationOnStorefront"/> + </test> +</tests> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml index b6417e12a6db7..89269a1ad0d9e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6325"/> <useCaseId value="MAGETWO-58764"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml index 19db201e91f40..85ec8f0bdaf6a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml @@ -23,6 +23,10 @@ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> @@ -87,6 +91,10 @@ <createData entity="_defaultProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> @@ -124,6 +132,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-15034"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,3); return ret;" stepKey="getFirstThreeLetters" before="searchStorefront"/> @@ -160,6 +169,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-14796"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <before> @@ -242,6 +252,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-14797"/> <group value="CatalogSearch"/> + <group value="SearchEngineMysql"/> <group value="mtf_migrated"/> </annotations> <before> @@ -306,6 +317,10 @@ <createData entity="VirtualProduct" stepKey="createVirtualProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> @@ -336,6 +351,10 @@ <argument name="product" value="_defaultProduct"/> <argument name="category" value="$$createCategory$$"/> </actionGroup> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> @@ -375,6 +394,10 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createProduct"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> @@ -405,6 +428,10 @@ <requiredEntity createDataKey="createProduct"/> <requiredEntity createDataKey="simple1"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> @@ -450,6 +477,10 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -512,6 +543,10 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -601,6 +636,10 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml new file mode 100644 index 0000000000000..9ad868ff6db7e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByAllParametersTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name, sku, description, short description, price from and price to"/> + <description value="Search product in advanced search by name, sku, description, short description, price from and price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + <argument name="description" value="adc_Full"/> + <argument name="short_description" value="abc_short"/> + <argument name="price_to" value="500"/> + <argument name="price_from" value="49"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml new file mode 100644 index 0000000000000..5693721e6ed65 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by description"/> + <description value="Search product in advanced search by description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct"/> + </before> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="description" value="dfj_full"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml new file mode 100644 index 0000000000000..4d3ba22f79356 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByNameSkuDescriptionPriceTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name, sku, description, short description, price from 49 and price to 50"/> + <description value="Search product in advanced search by name, sku, description, short description, price from 49 and price to 50"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + <argument name="description" value="adc_Full"/> + <argument name="short_description" value="abc_short"/> + <argument name="price_to" value="50"/> + <argument name="price_from" value="49"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml new file mode 100644 index 0000000000000..f0b81e08252fc --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByNameTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by name"/> + <description value="Search product in advanced search by name"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct"/> + </before> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml new file mode 100644 index 0000000000000..f875021bd9669 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPartialNameTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial name"/> + <description value="Search product in advanced search by partial name"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + <group value="SearchEngineMysql"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="abc"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml new file mode 100644 index 0000000000000..0edc3f31216bb --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPartialShortDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial short description"/> + <description value="Search product in advanced search by partial short description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="short_description" value="abc_short"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml new file mode 100644 index 0000000000000..b2b4ef9cc4782 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPartialSkuAndDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial sku and description"/> + <description value="Search product in advanced search by partial sku and description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc"/> + <argument name="description" value="adc_full"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml new file mode 100644 index 0000000000000..45cec0a899361 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPartialSkuTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by partial sku"/> + <description value="Search product in advanced search by partial sku"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml new file mode 100644 index 0000000000000..6b85cdf61c84c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPriceFromAndPriceToTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by price from and price to"/> + <description value="Search product in advanced search by price from and price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="price_to" value="50"/> + <argument name="price_from" value="50"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml new file mode 100644 index 0000000000000..755bb92c897ea --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByPriceToTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by price to"/> + <description value="Search product in advanced search by price to"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ABC_123_SimpleProduct" stepKey="createProduct2" after="createProduct"/> + </before> + <after> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2" after="deleteProduct"/> + </after> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="price_to" value="100"/> + </actionGroup> + <see userInput="2 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$createProduct2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProduct2Name" after="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml new file mode 100644 index 0000000000000..c4622d02a5152 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchByShortDescriptionTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by short description"/> + <description value="Search product in advanced search by short description"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <remove keyForRemoval="createProduct"/> + <remove keyForRemoval="deleteProduct"/> + <remove keyForRemoval="seeProductName"/> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="short_description" value="dfj_short"/> + </actionGroup> + <see userInput="We can't find any items matching these search criteria. Modify your search." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml new file mode 100644 index 0000000000000..ca5e237099681 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchBySkuTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Search product in advanced search by sku"/> + <description value="Search product in advanced search by sku"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="sku" value="abc_dfj"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml new file mode 100644 index 0000000000000..78110b531be33 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Use Advanced Search to Find the Product"/> + <description value="Use Advanced Search to Find the Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-12421"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="ABC_dfj_SimpleProduct" stepKey="createProduct"/> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- 1. Navigate to Frontend --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + + <!-- 2. Click "Advanced Search" --> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> + + <!-- 3. Fill test data in to field(s) 4. Click "Search" button--> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="sku" value="abc_dfj"/> + </actionGroup> + + <!-- 5. Perform all asserts --> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResult"/> + <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$createProduct.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.productName}}" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml new file mode 100644 index 0000000000000..b4f2314295a00 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchNegativeProductSearchTest" extends="StorefrontAdvancedSearchEntitySimpleProductTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Negative product search"/> + <description value="Negative product search"/> + <testCaseId value="MAGETWO-24729"/> + <severity value="CRITICAL"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <remove keyForRemoval="createProduct"/> + <remove keyForRemoval="deleteProduct"/> + <remove keyForRemoval="seeProductName"/> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> + <argument name="productName" value="Negative_product_search"/> + </actionGroup> + <see userInput="We can't find any items matching these search criteria. Modify your search." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml new file mode 100644 index 0000000000000..8a29ab718bd25 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvancedSearchWithoutEnteringDataTest"> + <annotations> + <stories value="Use Advanced Search"/> + <title value="Do Advanced Search without entering data"/> + <description value="'Enter a search term and try again.' error message is missed in Advanced Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-14859"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <!-- 1. Navigate to Frontend --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + + <!-- 2. Click "Advanced Search" --> + <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> + + <!-- 3. Fill test data in to field(s) 4. Click "Search" button--> + <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"/> + + <!-- 5. Perform all asserts --> + <see userInput="Enter a search term and try again." selector="{{StorefrontQuickSearchResultsSection.messageSection}}" stepKey="see"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php index abad58a6876d3..f783f75a170e3 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Test\Unit\Model\Layer\Filter; @@ -208,6 +209,12 @@ public function testApply() $priceId = '15-50'; $requestVar = 'test_request_var'; + $this->target->setAttributeModel($this->attribute); + $attributeCode = 'price'; + $this->attribute->expects($this->any()) + ->method('getAttributeCode') + ->will($this->returnValue($attributeCode)); + $this->target->setRequestVar($requestVar); $this->request->expects($this->exactly(1)) ->method('getParam') diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index f5e5a34047aff..10010188c26c9 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -183,6 +183,7 @@ public function testLike() 'searchResult' => $searchResult, 'orders' => [], 'size' => $pageSize, + 'currentPage' => 0, ] ) ->willReturn($searchResultApplier); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php index 8157c1fa8fa82..350344372612a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator; @@ -11,6 +12,9 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Test catalog search range request generator. + */ class DecimalTest extends \PHPUnit\Framework\TestCase { /** @var Decimal */ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php new file mode 100644 index 0000000000000..3635430197591 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogSearch\Model\Search\RequestGenerator\Price; +use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Search\Request\FilterInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test catalog search range request generator. + */ +class PriceTest extends \PHPUnit\Framework\TestCase +{ + /** @var Price */ + private $price; + + /** @var Attribute|\PHPUnit_Framework_MockObject_MockObject */ + private $attribute; + + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeConfigMock; + + protected function setUp() + { + $this->attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->setMethods(['getValue']) + ->getMockForAbstractClass(); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->price = $objectManager->getObject( + Price::class, + ['scopeConfig' => $this->scopeConfigMock] + ); + } + + public function testGetFilterData() + { + $filterName = 'test_filter_name'; + $attributeCode = 'test_attribute_code'; + $expected = [ + 'type' => FilterInterface::TYPE_RANGE, + 'name' => $filterName, + 'field' => $attributeCode, + 'from' => '$' . $attributeCode . '.from$', + 'to' => '$' . $attributeCode . '.to$', + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->price->getFilterData($this->attribute, $filterName); + $this->assertEquals($expected, $actual); + } + + public function testGetAggregationData() + { + $bucketName = 'test_bucket_name'; + $attributeCode = 'test_attribute_code'; + $method = 'price_dynamic_algorithm'; + $expected = [ + 'type' => BucketInterface::TYPE_DYNAMIC, + 'name' => $bucketName, + 'field' => $attributeCode, + 'method' => '$'. $method . '$', + 'metric' => [['type' => 'count']], + ]; + $this->attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $actual = $this->price->getAggregationData($this->attribute, $bucketName); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 28d5035308dee..da0a60dad1f77 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -281,6 +281,7 @@ <argument name="defaultGenerator" xsi:type="object">\Magento\CatalogSearch\Model\Search\RequestGenerator\General</argument> <argument name="generators" xsi:type="array"> <item name="decimal" xsi:type="object">Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal</item> + <item name="price" xsi:type="object">Magento\CatalogSearch\Model\Search\RequestGenerator\Price</item> </argument> </arguments> </type> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php index edca633fb14cc..d9e9705ac039d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php @@ -148,7 +148,7 @@ private function getCategoryUrlSuffix($storeId = null): string CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 704b60a8aaf2a..b1dfa79373a05 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -23,7 +23,6 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\ImportExport\Model\Import as ImportExport; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -252,7 +251,7 @@ public function execute(Observer $observer) * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _populateForUrlGeneration($rowData) + private function _populateForUrlGeneration($rowData) { $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]); $oldSku = $this->import->getOldSku(); @@ -321,7 +320,7 @@ private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): b * @param array $rowData * @return void */ - protected function setStoreToProduct(Product $product, array $rowData) + private function setStoreToProduct(Product $product, array $rowData) { if (!empty($rowData[ImportProduct::COL_STORE]) && ($storeId = $this->import->getStoreIdByCode($rowData[ImportProduct::COL_STORE])) @@ -339,7 +338,7 @@ protected function setStoreToProduct(Product $product, array $rowData) * @param string $storeId * @return $this */ - protected function addProductToImport($product, $storeId) + private function addProductToImport($product, $storeId) { if ($product->getVisibility() == (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]) { return $this; @@ -357,7 +356,7 @@ protected function addProductToImport($product, $storeId) * @param Product $product * @return $this */ - protected function populateGlobalProduct($product) + private function populateGlobalProduct($product) { foreach ($this->import->getProductWebsites($product->getSku()) as $websiteId) { foreach ($this->websitesToStoreIds[$websiteId] as $storeId) { @@ -376,7 +375,7 @@ protected function populateGlobalProduct($product) * @return UrlRewrite[] * @throws LocalizedException */ - protected function generateUrls() + private function generateUrls() { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate()); @@ -398,7 +397,7 @@ protected function generateUrls() * @param int|null $storeId * @return bool */ - protected function isGlobalScope($storeId) + private function isGlobalScope($storeId) { return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; } @@ -408,7 +407,7 @@ protected function isGlobalScope($storeId) * * @return UrlRewrite[] */ - protected function canonicalUrlRewriteGenerate() + private function canonicalUrlRewriteGenerate() { $urls = []; foreach ($this->products as $productId => $productsByStores) { @@ -433,7 +432,7 @@ protected function canonicalUrlRewriteGenerate() * @return UrlRewrite[] * @throws LocalizedException */ - protected function categoriesUrlRewriteGenerate() + private function categoriesUrlRewriteGenerate(): array { $urls = []; foreach ($this->products as $productId => $productsByStores) { @@ -444,17 +443,24 @@ protected function categoriesUrlRewriteGenerate() continue; } $requestPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); - $urls[] = $this->urlRewriteFactory->create() - ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($productId) - ->setRequestPath($requestPath) - ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category)) - ->setStoreId($storeId) - ->setMetadata(['category_id' => $category->getId()]); + $urls[] = [ + $this->urlRewriteFactory->create() + ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($productId) + ->setRequestPath($requestPath) + ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category)) + ->setStoreId($storeId) + ->setMetadata(['category_id' => $category->getId()]) + ]; + $parentCategoryIds = $category->getAnchorsAbove(); + if ($parentCategoryIds) { + $urls[] = $this->getParentCategoriesUrlRewrites($parentCategoryIds, $storeId, $product); + } } } } - return $urls; + $result = !empty($urls) ? array_merge(...$urls) : []; + return $result; } /** @@ -462,7 +468,7 @@ protected function categoriesUrlRewriteGenerate() * * @return UrlRewrite[] */ - protected function currentUrlRewritesRegenerate() + private function currentUrlRewritesRegenerate() { $currentUrlRewrites = $this->urlFinder->findAllByData( [ @@ -496,7 +502,7 @@ protected function currentUrlRewritesRegenerate() * @param Category $category * @return array */ - protected function generateForAutogenerated($url, $category) + private function generateForAutogenerated($url, $category) { $storeId = $url->getStoreId(); $productId = $url->getEntityId(); @@ -532,7 +538,7 @@ protected function generateForAutogenerated($url, $category) * @param Category $category * @return array */ - protected function generateForCustom($url, $category) + private function generateForCustom($url, $category) { $storeId = $url->getStoreId(); $productId = $url->getEntityId(); @@ -566,7 +572,7 @@ protected function generateForCustom($url, $category) * @param UrlRewrite $url * @return Category|null|bool */ - protected function retrieveCategoryFromMetadata($url) + private function retrieveCategoryFromMetadata($url) { $metadata = $url->getMetadata(); if (isset($metadata['category_id'])) { @@ -576,32 +582,6 @@ protected function retrieveCategoryFromMetadata($url) return null; } - /** - * Check, category suited for url-rewrite generation. - * - * @param Category $category - * @param int $storeId - * @return bool - * @throws NoSuchEntityException - */ - protected function isCategoryProperForGenerating($category, $storeId) - { - if (isset($this->acceptableCategories[$storeId]) && - isset($this->acceptableCategories[$storeId][$category->getId()])) { - return $this->acceptableCategories[$storeId][$category->getId()]; - } - $acceptable = false; - if ($category->getParentId() != Category::TREE_ROOT_ID) { - list(, $rootCategoryId) = $category->getParentIds(); - $acceptable = ($rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId()); - } - if (!isset($this->acceptableCategories[$storeId])) { - $this->acceptableCategories[$storeId] = []; - } - $this->acceptableCategories[$storeId][$category->getId()] = $acceptable; - return $acceptable; - } - /** * Get category by id considering store scope. * @@ -635,4 +615,36 @@ private function isCategoryRewritesEnabled() { return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites'); } + + /** + * Generate url-rewrite for anchor parent-categories. + * + * @param array $categoryIds + * @param int $storeId + * @param Product $product + * @return array + * @throws LocalizedException + */ + private function getParentCategoriesUrlRewrites(array $categoryIds, int $storeId, Product $product): array + { + $urls = []; + foreach ($categoryIds as $categoryId) { + $category = $this->getCategoryById($categoryId, $storeId); + if ($category->getParentId() == Category::TREE_ROOT_ID) { + continue; + } + $requestPath = $this->productUrlPathGenerator + ->getUrlPathWithSuffix($product, $storeId, $category); + $targetPath = $this->productUrlPathGenerator + ->getCanonicalUrlPath($product, $category); + $urls[] = $this->urlRewriteFactory->create() + ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) + ->setEntityId($product->getId()) + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setStoreId($storeId) + ->setMetadata(['category_id' => $category->getId()]); + } + return $urls; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index 7f987124040fd..54ef7102fcc47 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; @@ -18,6 +20,14 @@ */ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { + + /** + * Reserved endpoint names. + * + * @var string[] + */ + private $invalidValues = []; + /** * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator */ @@ -38,22 +48,34 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface */ private $categoryRepository; + /** + * @var \Magento\Backend\App\Area\FrontNameResolver + */ + private $frontNameResolver; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService * @param CategoryRepositoryInterface $categoryRepository + * @param \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver + * @param string[] $invalidValues */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, ChildrenCategoriesProvider $childrenCategoriesProvider, StoreViewService $storeViewService, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver = null, + array $invalidValues = [] ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->storeViewService = $storeViewService; $this->categoryRepository = $categoryRepository; + $this->frontNameResolver = $frontNameResolver ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Backend\App\Area\FrontNameResolver::class); + $this->invalidValues = $invalidValues; } /** @@ -93,6 +115,17 @@ private function updateUrlKey($category, $urlKey) if (empty($urlKey)) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); } + + if (in_array($urlKey, $this->getInvalidValues())) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'URL key "%1" matches a reserved endpoint name (%2). Use another URL key.', + $urlKey, + implode(', ', $this->getInvalidValues()) + ) + ); + } + $category->setUrlKey($urlKey) ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); if (!$category->isObjectNew()) { @@ -103,6 +136,16 @@ private function updateUrlKey($category, $urlKey) } } + /** + * Get reserved endpoint names. + * + * @return array + */ + private function getInvalidValues() + { + return array_unique(array_merge($this->invalidValues, [$this->frontNameResolver->getFrontName()])); + } + /** * Update url path for children category. * diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml new file mode 100644 index 0000000000000..b463b0524d5ff --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminCategoryRestrictedUrlErrorMessage"> + <data key="urlAdmin">URL key "admin" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlSoap">URL key "soap" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlRest">URL key "rest" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + <data key="urlGraphql">URL key "graphql" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml new file mode 100644 index 0000000000000..6538ec6e935df --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCategoryWithRestrictedUrlKeyNotCreatedTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <stories value="Url rewrites"/> + <title value="Category with restricted Url Key cannot be created"/> + <description value="Category with restricted Url Key cannot be created"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17515"/> + <useCaseId value="MAGETWO-69825"/> + <group value="CatalogUrlRewrite"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created categories--> + <comment userInput="Delete created categories" stepKey="commentDeleteCreatedCategories"/> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteAdminCategory"> + <argument name="categoryName" value="admin"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteSoapCategory"> + <argument name="categoryName" value="soap"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteRestCategory"> + <argument name="categoryName" value="rest"/> + </actionGroup> + <actionGroup ref="AdminDeleteCategoryByName" stepKey="deleteGraphQlCategory"> + <argument name="categoryName" value="graphql"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Check category creation with restricted url key 'admin'--> + <comment userInput="Check category creation with restricted url key 'admin'" stepKey="commentCheckAdminCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateAdminCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminFirstCategoryForm"> + <argument name="categoryName" value="admin"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlAdmin}}' stepKey="seeAdminFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminSecondCategoryForm"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + <argument name="categoryUrlKey" value="admin"/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlAdmin}}' stepKey="seeAdminSecondErrorMessage"/> + <!--Create category with 'admin' name--> + <comment userInput="Create category with 'admin' name" stepKey="commentAdminCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillAdminThirdCategoryForm"> + <argument name="categoryName" value="admin"/> + <argument name="categoryUrlKey" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the category." stepKey="seeAdminSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('admin')}}" stepKey="seeAdminCategoryInTree"/> + <!--Check category creation with restricted url key 'soap'--> + <comment userInput="Check category creation with restricted url key 'soap'" stepKey="commentCheckSoapCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateSoapCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapFirstCategoryForm"> + <argument name="categoryName" value="soap"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlSoap}}' stepKey="seeSoapFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapSecondCategoryForm"> + <argument name="categoryName" value="{{ApiCategory.name}}"/> + <argument name="categoryUrlKey" value="soap"/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlSoap}}' stepKey="seeSoapSecondErrorMessage"/> + <!--Create category with 'soap' name--> + <comment userInput="Create category with 'soap' name" stepKey="commentSoapCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillSoapThirdCategoryForm"> + <argument name="categoryName" value="soap"/> + <argument name="categoryUrlKey" value="{{ApiCategory.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the category." stepKey="seeSoapSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('soap')}}" stepKey="seeSoapCategoryInTree"/> + <!--Check category creation with restricted url key 'rest'--> + <comment userInput="Check category creation with restricted url key 'rest'" stepKey="commentCheckRestCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateRestCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestFirstCategoryForm"> + <argument name="categoryName" value="rest"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlRest}}' stepKey="seeRestFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestSecondCategoryForm"> + <argument name="categoryName" value="{{SubCategoryWithParent.name}}"/> + <argument name="categoryUrlKey" value="rest"/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlRest}}' stepKey="seeRestSecondErrorMessage"/> + <!--Create category with 'rest' name--> + <comment userInput="Create category with 'rest' name" stepKey="commentRestCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillRestThirdCategoryForm"> + <argument name="categoryName" value="rest"/> + <argument name="categoryUrlKey" value="{{SubCategoryWithParent.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the category." stepKey="seeRestSuccessMesdgssage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('rest')}}" stepKey="seeRestCategoryInTree"/> + <!--Check category creation with restricted url key 'graphql'--> + <comment userInput="Check category creation with restricted url key 'graphql'" stepKey="commentCheckGraphQlCategoryCreation"/> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlFirstCategoryForm"> + <argument name="categoryName" value="graphql"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlGraphql}}' stepKey="seeGraphQlFirstErrorMessage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlSecondCategoryForm"> + <argument name="categoryName" value="{{NewSubCategoryWithParent.name}}"/> + <argument name="categoryUrlKey" value="graphql"/> + </actionGroup> + <see selector="{{AdminMessagesSection.errorMessage}}" userInput='{{AdminCategoryRestrictedUrlErrorMessage.urlGraphql}}' stepKey="seeGraphQlSecondErrorMessage"/> + <!--Create category with 'graphql' name--> + <comment userInput="Create category with 'graphql' name" stepKey="commentGraphQlCategoryCreation"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillGraphQlThirdCategoryForm"> + <argument name="categoryName" value="graphql"/> + <argument name="categoryUrlKey" value="{{NewSubCategoryWithParent.name}}"/> + </actionGroup> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the category." stepKey="seeGraphQlSuccessMessage"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryByName('graphql')}}" stepKey="seeGraphQlCategoryInTree"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index 3984d949332d3..94fe6ae8c54dc 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -153,24 +153,32 @@ class AfterImportDataObserverTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->importProduct = $this->createPartialMock(\Magento\CatalogImportExport\Model\Import\Product::class, [ + $this->importProduct = $this->createPartialMock( + \Magento\CatalogImportExport\Model\Import\Product::class, + [ 'getNewSku', 'getProductCategories', 'getProductWebsites', 'getStoreIdByCode', 'getCategoryProcessor', - ]); - $this->catalogProductFactory = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, [ + ] + ); + $this->catalogProductFactory = $this->createPartialMock( + \Magento\Catalog\Model\ProductFactory::class, + [ 'create', - ]); + ] + ); $this->storeManager = $this ->getMockBuilder( \Magento\Store\Model\StoreManagerInterface::class ) ->disableOriginalConstructor() - ->setMethods([ - 'getWebsite', - ]) + ->setMethods( + [ + 'getWebsite', + ] + ) ->getMockForAbstractClass(); $this->event = $this->createPartialMock(\Magento\Framework\Event::class, ['getAdapter', 'getBunch']); $this->event->expects($this->any())->method('getAdapter')->willReturn($this->importProduct); @@ -202,9 +210,11 @@ protected function setUp() ); $this->urlFinder = $this ->getMockBuilder(\Magento\UrlRewrite\Model\UrlFinderInterface::class) - ->setMethods([ - 'findAllByData', - ]) + ->setMethods( + [ + 'findAllByData', + ] + ) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -269,9 +279,12 @@ public function testAfterImportData() $newSku = [['entity_id' => 'value'], ['entity_id' => 'value3']]; $websiteId = 'websiteId value'; $productsCount = count($this->products); - $websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, [ + $websiteMock = $this->createPartialMock( + \Magento\Store\Model\Website::class, + [ 'getStoreIds', - ]); + ] + ); $storeIds = [1, Store::DEFAULT_STORE_ID]; $websiteMock ->expects($this->once()) @@ -315,13 +328,16 @@ public function testAfterImportData() ->expects($this->exactly(1)) ->method('getStoreIdByCode') ->will($this->returnValueMap($map)); - $product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + $product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ 'getId', 'setId', 'getSku', 'setStoreId', 'getStoreId', - ]); + ] + ); $product ->expects($this->exactly($productsCount)) ->method('setId') @@ -341,17 +357,21 @@ public function testAfterImportData() $product ->expects($this->exactly($productsCount)) ->method('getSku') - ->will($this->onConsecutiveCalls( - $this->products[0]['sku'], - $this->products[1]['sku'] - )); + ->will( + $this->onConsecutiveCalls( + $this->products[0]['sku'], + $this->products[1]['sku'] + ) + ); $product ->expects($this->exactly($productsCount)) ->method('getStoreId') - ->will($this->onConsecutiveCalls( - $this->products[0][ImportProduct::COL_STORE], - $this->products[1][ImportProduct::COL_STORE] - )); + ->will( + $this->onConsecutiveCalls( + $this->products[0][ImportProduct::COL_STORE], + $this->products[1][ImportProduct::COL_STORE] + ) + ); $product ->expects($this->exactly($productsCount)) ->method('setStoreId') @@ -540,7 +560,10 @@ public function testCategoriesUrlRewriteGenerate() ->expects($this->any()) ->method('getId') ->will($this->returnValue($this->categoryId)); - + $category + ->expects($this->any()) + ->method('getAnchorsAbove') + ->willReturn([]); $categoryCollection = $this->getMockBuilder(CategoryCollection::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index e6fbcaefd0768..2a74b5cd92b28 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -45,4 +45,15 @@ </argument> </arguments> </type> + <type name="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"> + <arguments> + <argument name="invalidValues" xsi:type="array"> + <item name="0" xsi:type="string">admin</item> + <item name="1" xsi:type="string">soap</item> + <item name="2" xsi:type="string">rest</item> + <item name="3" xsi:type="string">graphql</item> + <item name="4" xsi:type="string">standard</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv index b3335dc3523ca..0f21e8ddf9fc9 100644 --- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv +++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv @@ -5,4 +5,5 @@ "Product URL Suffix","Product URL Suffix" "Use Categories Path for Product URLs","Use Categories Path for Product URLs" "Create Permanent Redirect for URLs if URL Key Changed","Create Permanent Redirect for URLs if URL Key Changed" -"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites" \ No newline at end of file +"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites" +"URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key.","URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key." diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php new file mode 100644 index 0000000000000..59708d90c23b7 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Returns the url suffix for category + */ +class CategoryUrlSuffix implements ResolverInterface +{ + /** + * System setting for the url suffix for categories + * + * @var string + */ + private static $xml_path_category_url_suffix = 'catalog/seo/category_url_suffix'; + + /** + * Cache for product rewrite suffix + * + * @var array + */ + private $categoryUrlSuffix = []; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): string { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->getCategoryUrlSuffix($storeId); + } + + /** + * Retrieve category url suffix by store + * + * @param int $storeId + * @return string + */ + private function getCategoryUrlSuffix(int $storeId): string + { + if (!isset($this->categoryUrlSuffix[$storeId])) { + $this->categoryUrlSuffix[$storeId] = $this->scopeConfig->getValue( + self::$xml_path_category_url_suffix, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + return $this->categoryUrlSuffix[$storeId]; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php new file mode 100644 index 0000000000000..9a0193ba36367 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Returns the url suffix for product + */ +class ProductUrlSuffix implements ResolverInterface +{ + /** + * System setting for the url suffix for products + * + * @var string + */ + private static $xml_path_product_url_suffix = 'catalog/seo/product_url_suffix'; + + /** + * Cache for product rewrite suffix + * + * @var array + */ + private $productUrlSuffix = []; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): string { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + return $this->getProductUrlSuffix($storeId); + } + + /** + * Retrieve product url suffix by store + * + * @param int $storeId + * @return string + */ + private function getProductUrlSuffix(int $storeId): string + { + if (!isset($this->productUrlSuffix[$storeId])) { + $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( + self::$xml_path_product_url_suffix, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + return $this->productUrlSuffix[$storeId]; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json index e276da0cc6fd8..202c573c2ae04 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json @@ -4,6 +4,7 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/module-store": "*", "magento/module-catalog": "*", "magento/framework": "*" }, diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index 4453674de04dd..82facf6959f3c 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -3,10 +3,15 @@ interface ProductInterface { url_key: String @doc(description: "The part of the URL that identifies the product") + url_suffix: String @doc(description: "The part of the product URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\ProductUrlSuffix") url_path: String @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") } +interface CategoryInterface { + url_suffix: String @doc(description: "The part of the category URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\CategoryUrlSuffix") +} + input ProductFilterInput { url_key: FilterTypeInput @doc(description: "The part of the URL that identifies the product") url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead") diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index e5fb20a58aea1..a712ae91cbfa9 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -11,6 +11,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ProductCategoryList; +use Magento\Store\Model\Store; /** * Class Product @@ -106,7 +107,7 @@ function ($attribute) { } /** - * {@inheritdoc} + * @inheritdoc * * @param array &$attributes * @return void @@ -164,6 +165,8 @@ public function addToCollection($collection) } /** + * Adds Attributes that belong to Global Scope + * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @return $this @@ -200,6 +203,8 @@ protected function addGlobalAttribute( } /** + * Adds Attributes that don't belong to Global Scope + * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @return $this @@ -208,7 +213,7 @@ protected function addNotGlobalAttribute( \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute, \Magento\Catalog\Model\ResourceModel\Product\Collection $collection ) { - $storeId = $this->storeManager->getStore()->getId(); + $storeId = $this->storeManager->getStore()->getId(); $values = $collection->getAllAttributeValues($attribute); $validEntities = []; if ($values) { @@ -218,7 +223,9 @@ protected function addNotGlobalAttribute( $validEntities[] = $entityId; } } else { - if ($this->validateAttribute($storeValues[\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { + if (isset($storeValues[Store::DEFAULT_STORE_ID]) && + $this->validateAttribute($storeValues[Store::DEFAULT_STORE_ID]) + ) { $validEntities[] = $entityId; } } @@ -236,7 +243,7 @@ protected function addNotGlobalAttribute( } /** - * {@inheritdoc} + * @inheritdoc * * @return string */ @@ -257,7 +264,7 @@ public function getMappedSqlField() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection * @return $this diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml new file mode 100644 index 0000000000000..9ff7e5a96fae7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontCheckoutSuccessActionGroup"> + <annotations> + <description>Verifies if the order is placed successfully on the 'one page checkout' page.</description> + </annotations> + <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml index 176eebed142c8..8933ebbc1dd84 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml @@ -18,12 +18,24 @@ <argument name="cartSubtotal" type="string"/> <argument name="qty" type="string"/> </arguments> - - <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeProductNameInMiniCart"/> - <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productPrice}}" stepKey="seeProductPriceInMiniCart"/> + + <see selector="{{StorefrontMinicartSection.productPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="seeProductPriceInMiniCart"/> <seeElement selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="seeCheckOutButtonInMiniCart"/> <seeElement selector="{{StorefrontMinicartSection.productQuantity(productName, qty)}}" stepKey="seeProductQuantity1"/> <seeElement selector="{{StorefrontMinicartSection.productImage}}" stepKey="seeProductImage"/> <see selector="{{StorefrontMinicartSection.productSubTotal}}" userInput="{{cartSubtotal}}" stepKey="seeSubTotal"/> </actionGroup> + + <actionGroup name="AssertStorefrontMiniCartProductDetailsAbsentActionGroup"> + <annotations> + <description>Validates that the provided Product details (Name, Price) are + not present in the Storefront Mini Shopping Cart.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + <argument name="productPrice" type="string"/> + </arguments> + + <dontSee selector="{{StorefrontMinicartSection.productPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="dontSeeProductPriceInMiniCart"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 7f6980d0c9744..e0519a126aa48 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -64,6 +64,7 @@ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="waitForShippingMethod"/> <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> @@ -221,7 +222,7 @@ <!-- Check product in checkout cart items --> <actionGroup name="CheckProductInCheckoutCartItemsActionGroup"> <annotations> - <description>Validates the the provided Product appears in the Storefront Checkout 'Order Summary' section.</description> + <description>Validates the provided Product appears in the Storefront Checkout 'Order Summary' section.</description> </annotations> <arguments> <argument name="productVar"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index 789a61a1700db..77734cc75497f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -28,6 +28,10 @@ <fillField selector="{{CheckoutPaymentSection.guestPostcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutPaymentSection.guestTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> </actionGroup> + <actionGroup name="StorefrontCheckoutFillNewBillingAddressActionGroup" extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="enterEmail"/> + <remove keyForRemoval="waitForLoading3"/> + </actionGroup> <actionGroup name="LoggedInCheckoutFillNewBillingAddressActionGroup"> <annotations> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml index 112abfbb5897a..9f766742b545f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml @@ -17,13 +17,12 @@ <argument name="total" type="string"/> <argument name="discount" type="string"/> </arguments> - <waitForPageLoad stepKey="waitForPageLoad"/> - <waitForLoadingMaskToDisappear stepKey="waitForPrices"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalElement"/> + <see selector="{{CheckoutCartSummarySection.total}}" userInput="${{total}}" stepKey="assertTotal"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-${{discount}}" stepKey="assertDiscount"/> - <wait time="10" stepKey="waitForTotalPrice"/> - <reloadPage stepKey="reloadPage" after="waitForTotalPrice" /> - <waitForPageLoad after="reloadPage" stepKey="WaitForPageLoaded" /> - <see selector="{{CheckoutCartSummarySection.total}}" userInput="${{total}}" stepKey="assertTotal" after="WaitForPageLoaded"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml index d3fa045e4654f..d6173dfa17916 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml @@ -14,5 +14,6 @@ <section name="CheckoutOrderSummarySection"/> <section name="CheckoutSuccessMainSection"/> <section name="CheckoutPaymentSection"/> + <section name="SelectShippingBillingPopupSection"/> </page> </pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 3ab3fa5857b78..1b85f3b045c5d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -18,6 +18,7 @@ <element name="ProductRegularPriceByName" type="text" selector="//div[descendant::*[contains(text(), '{{var1}}')]]//*[contains(@class, 'subtotal')]" parameterized="true"/> + <element name="productFirstPrice" type="text" selector="td[class~=price] span[class='price']"/> <element name="ProductImageByName" type="text" selector="//main//table[@id='shopping-cart-table']//tbody//tr//img[contains(@class, 'product-image-photo') and @alt='{{var1}}']" parameterized="true"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index 477451ef003ce..20b71608cd038 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -20,7 +20,7 @@ <element name="totalAmount" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price' and contains(text(), '{{amount}}')]" parameterized="true"/> <element name="proceedToCheckout" type="button" selector=".action.primary.checkout span" timeout="30"/> <element name="discountAmount" type="text" selector="td[data-th='Discount']"/> - <element name="shippingHeading" type="button" selector="#block-shipping-heading" timeout="60"/> + <element name="shippingHeading" type="button" selector="#block-shipping-heading" timeout="10"/> <element name="postcode" type="input" selector="input[name='postcode']" timeout="10"/> <element name="stateProvince" type="select" selector="select[name='region_id']" timeout="10"/> <element name="stateProvinceInput" type="input" selector="input[name='region']"/> @@ -33,5 +33,6 @@ <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> + <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 903c21d7ec0ca..be8519f920b90 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -31,7 +31,7 @@ <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active" timeout="30"/> <element name="checkMoneyOrderPayment" type="radio" selector="input#checkmo.radio" timeout="30"/> <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> - <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[text()='Payment Method']" /> + <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[@data-role='title']" /> <element name="orderSummarySubtotal" type="text" selector="//tr[@class='totals sub']//span[@class='price']" /> <element name="orderSummaryShippingTotal" type="text" selector="//tr[@class='totals shipping excl']//span[@class='price']" /> <element name="orderSummaryShippingMethod" type="text" selector="//tr[@class='totals shipping excl']//span[@class='value']" /> @@ -42,6 +42,7 @@ <element name="ProductOptionLinkActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']//a[text() = '{{var2}}']" parameterized="true" /> <element name="shipToInformation" type="text" selector="//div[@class='ship-to']//div[@class='shipping-information-content']" /> <element name="shippingMethodInformation" type="text" selector="//div[@class='ship-via']//div[@class='shipping-information-content']" /> + <element name="shippingInformationSection" type="text" selector=".ship-to .shipping-information-content" /> <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> @@ -51,6 +52,7 @@ <element name="orderSummaryTotalIncluding" type="text" selector="//tr[@class='grand totals incl']//span[@class='price']" /> <element name="orderSummaryTotalExcluding" type="text" selector="//tr[@class='grand totals excl']//span[@class='price']" /> <element name="shippingAndBillingAddressSame" type="input" selector="#billing-address-same-as-shipping-braintree_cc_vault"/> + <element name="myShippingAndBillingAddressSame" type="input" selector=".billing-address-same-as-shipping-block"/> <element name="addressAction" type="button" selector="//span[text()='{{action}}']" parameterized="true"/> <element name="addressBook" type="button" selector="//a[text()='Address Book']"/> <element name="noQuotes" type="text" selector=".no-quotes-block"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index 3e1de2b14ba62..e00906386e46b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -13,7 +13,9 @@ <element name="productCount" type="text" selector="//header//div[contains(@class, 'minicart-wrapper')]//a[contains(@class, 'showcart')]//span[@class='counter-number']"/> <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> - <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productPriceByItsName" type="text" selector="//a[normalize-space()='{{prodName}}']/../following-sibling::*//*[@class='price']" parameterized="true"/> + <element name="productImageByName" type="text" selector="header ol[id='mini-cart'] span[class='product-image-container'] img[alt='{{prodName}}']" parameterized="true"/> + <element name="productImageByItsName" type="text" selector="img[alt='{{prodName}}']" parameterized="true"/> <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml index 9714b76a05613..163e71c50053f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml @@ -30,7 +30,7 @@ <!--Logout from customer account--> <amOnPage url="customer/account/logout/" stepKey="logoutCustomerOne"/> <waitForPageLoad stepKey="waitLogoutCustomerOne"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> </after> @@ -147,7 +147,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index 20015f76e08e3..c61545e51d535 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -25,6 +25,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + <!--Clear cache and reindex--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 5335ec2ad775d..4281a0eb77da8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -184,6 +184,194 @@ <argument name="productVar" value="$$createSimpleProduct2$$"/> </actionGroup> + <comment userInput="Place order with check money order payment" stepKey="commentPlaceOrderWithCheckMoneyOrderPayment" after="guestCheckoutCheckSimpleProduct2InCartItems" /> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" after="commentPlaceOrderWithCheckMoneyOrderPayment"/> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="guestSeeBillingAddress" after="guestSelectCheckMoneyOrderPayment"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceorder" after="guestSeeBillingAddress"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <comment userInput="End of checking out" stepKey="endOfCheckingOut" after="guestPlaceorder" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <!-- Step 3: User adds products to cart --> + <comment userInput="Start of adding products to cart" stepKey="startOfAddingProductsToCart" after="endOfBrowsingCatalog"/> + <!-- Add Simple Product 1 to cart --> + <comment userInput="Add Simple Product 1 to cart" stepKey="commentAddSimpleProduct1ToCart" after="startOfAddingProductsToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory" after="commentAddSimpleProduct1ToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategoryloaded" after="cartClickCategory"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory" after="waitForCartCategoryloaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="cartAssertSimpleProduct1" after="cartAssertCategory"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartGrabSimpleProduct1ImageSrc" after="cartAssertSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabSimpleProduct1ImageSrc" stepKey="cartAssertSimpleProduct1ImageNotDefault" after="cartGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct1.name$$)}}" stepKey="cartClickSimpleProduct1" after="cartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct1loaded" after="cartClickSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertProduct1Page" after="waitForCartSimpleProduct1loaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabSimpleProduct1PageImageSrc" after="cartAssertProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabSimpleProduct1PageImageSrc" stepKey="cartAssertSimpleProduct1PageImageNotDefault" after="cartGrabSimpleProduct1PageImageSrc"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddProduct1ToCart" after="cartAssertSimpleProduct1PageImageNotDefault"> + <argument name="product" value="$$createSimpleProduct1$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Add Simple Product 2 to cart --> + <comment userInput="Add Simple Product 2 to cart" stepKey="commentAddSimpleProduct2ToCart" after="cartAddProduct1ToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory1" after="commentAddSimpleProduct2ToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategory1loaded" after="cartClickCategory1"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory1ForSimpleProduct2" after="waitForCartCategory1loaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="cartAssertSimpleProduct2" after="cartAssertCategory1ForSimpleProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartGrabSimpleProduct2ImageSrc" after="cartAssertSimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabSimpleProduct2ImageSrc" stepKey="cartAssertSimpleProduct2ImageNotDefault" after="cartGrabSimpleProduct2ImageSrc"/> + <actionGroup ref="StorefrontAddCategoryProductToCartActionGroup" stepKey="cartAddProduct2ToCart" after="cartAssertSimpleProduct2ImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productCount" value="CONST.two"/> + </actionGroup> + + <!-- Check products in minicart --> + <!-- Check simple product 1 in minicart --> + <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="cartAddProduct2ToCart"/> + <actionGroup ref="StorefrontOpenMinicartAndCheckSimpleProductActionGroup" stepKey="cartOpenMinicartAndCheckSimpleProduct1" after="commentCheckSimpleProduct1InMinicart"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct1ImageSrc" after="cartOpenMinicartAndCheckSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabSimpleProduct1ImageSrc" stepKey="cartMinicartAssertSimpleProduct1ImageNotDefault" after="cartMinicartGrabSimpleProduct1ImageSrc"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createSimpleProduct1.name$$)}}" stepKey="cartMinicartClickSimpleProduct1" after="cartMinicartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartSimpleProduct1loaded" after="cartMinicartClickSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertMinicartProduct1Page" after="waitForMinicartSimpleProduct1loaded"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct1PageImageSrc" after="cartAssertMinicartProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabSimpleProduct1PageImageSrc" stepKey="cartMinicartAssertSimpleProduct1PageImageNotDefault" after="cartMinicartGrabSimpleProduct1PageImageSrc"/> + <actionGroup ref="StorefrontOpenMinicartAndCheckSimpleProductActionGroup" stepKey="cartOpenMinicartAndCheckSimpleProduct2" after="cartMinicartAssertSimpleProduct1PageImageNotDefault"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- Check simple product2 in minicart --> + <comment userInput="Check simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="cartOpenMinicartAndCheckSimpleProduct2"/> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct2ImageSrc" after="commentCheckSimpleProduct2InMinicart"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabSimpleProduct2ImageSrc" stepKey="cartMinicartAssertSimpleProduct2ImageNotDefault" after="cartMinicartGrabSimpleProduct2ImageSrc"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createSimpleProduct2.name$$)}}" stepKey="cartMinicartClickSimpleProduct2" after="cartMinicartAssertSimpleProduct2ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartSimpleProduct2loaded" after="cartMinicartClickSimpleProduct2"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertMinicartProduct2Page" after="waitForMinicartSimpleProduct2loaded"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabSimpleProduct2PageImageSrc" after="cartAssertMinicartProduct2Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabSimpleProduct2PageImageSrc" stepKey="cartMinicartAssertSimpleProduct2PageImageNotDefault" after="cartMinicartGrabSimpleProduct2PageImageSrc"/> + + <!-- Check products in cart --> + <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + + <!-- Check simple product 1 in cart --> + <comment userInput="Check simple product 1 in cart" stepKey="commentCheckSimpleProduct1InCart" after="cartAssertCart"/> + <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="cartAssertCartSimpleProduct1" after="commentCheckSimpleProduct1InCart"> + <argument name="product" value="$$createSimpleProduct1$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createSimpleProduct1.name$$)}}" userInput="src" stepKey="cartCartGrabSimpleProduct1ImageSrc" after="cartAssertCartSimpleProduct1"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabSimpleProduct1ImageSrc" stepKey="cartCartAssertSimpleProduct1ImageNotDefault" after="cartCartGrabSimpleProduct1ImageSrc"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct1.name$$)}}" stepKey="cartClickCartSimpleProduct1" after="cartCartAssertSimpleProduct1ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct1loadedAgain" after="cartClickCartSimpleProduct1"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertCartProduct1Page" after="waitForCartSimpleProduct1loadedAgain"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabSimpleProduct2PageImageSrc1" after="cartAssertCartProduct1Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabSimpleProduct2PageImageSrc1" stepKey="cartCartAssertSimpleProduct2PageImageNotDefault1" after="cartCartGrabSimpleProduct2PageImageSrc1"/> + + <!-- Check simple product 2 in cart --> + <comment userInput="Check simple product 2 in cart" stepKey="commentCheckSimpleProduct2InCart" after="cartCartAssertSimpleProduct2PageImageNotDefault1"/> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart1" after="commentCheckSimpleProduct2InCart"/> + <actionGroup ref="StorefrontCheckCartSimpleProductActionGroup" stepKey="cartAssertCartSimpleProduct2" after="cartOpenCart1"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createSimpleProduct2.name$$)}}" userInput="src" stepKey="cartCartGrabSimpleProduct2ImageSrc" after="cartAssertCartSimpleProduct2"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabSimpleProduct2ImageSrc" stepKey="cartCartAssertSimpleProduct2ImageNotDefault" after="cartCartGrabSimpleProduct2ImageSrc"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct2.name$$)}}" stepKey="cartClickCartSimpleProduct2" after="cartCartAssertSimpleProduct2ImageNotDefault"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartSimpleProduct2loaded" after="cartClickCartSimpleProduct2"/> + <actionGroup ref="StorefrontCheckSimpleProduct" stepKey="cartAssertCartProduct2Page" after="waitForCartSimpleProduct2loaded"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabSimpleProduct2PageImageSrc2" after="cartAssertCartProduct2Page"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabSimpleProduct2PageImageSrc2" stepKey="cartCartAssertSimpleProduct2PageImageNotDefault2" after="cartCartGrabSimpleProduct2PageImageSrc2"/> + <comment userInput="End of adding products to cart" stepKey="endOfAddingProductsToCart" after="cartCartAssertSimpleProduct2PageImageNotDefault2" /> + + <!-- Step 6: Check out --> + <comment userInput="Start of checking out" stepKey="startOfCheckingOut" after="endOfUsingCouponCode" /> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" after="startOfCheckingOut"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection" after="guestGoToCheckoutFromMinicart"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!-- Check order summary in checkout --> + <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="guestCheckoutFillingShippingSection" /> + <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="guestCheckoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + + <!-- Check ship to information in checkout --> + <comment userInput="Check ship to information in checkout" stepKey="commentCheckShipToInformationInCheckout" after="guestCheckoutCheckOrderSummary" /> + <actionGroup ref="CheckShipToInformationInCheckoutActionGroup" stepKey="guestCheckoutCheckShipToInformation" after="commentCheckShipToInformationInCheckout"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + </actionGroup> + + <!-- Check shipping method in checkout --> + <comment userInput="Check shipping method in checkout" stepKey="commentCheckShippingMethodInCheckout" after="guestCheckoutCheckShipToInformation" /> + <actionGroup ref="CheckShippingMethodInCheckoutActionGroup" stepKey="guestCheckoutCheckShippingMethod" after="commentCheckShippingMethodInCheckout"> + <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod" /> + </actionGroup> + + <!-- Verify Simple Product 1 is in checkout cart items --> + <comment userInput="Verify Simple Product 1 is in checkout cart items" stepKey="commentVerifySimpleProduct1IsInCheckoutCartItems" after="guestCheckoutCheckShippingMethod" /> + <actionGroup ref="CheckProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckSimpleProduct1InCartItems" after="commentVerifySimpleProduct1IsInCheckoutCartItems"> + <argument name="productVar" value="$$createSimpleProduct1$$"/> + </actionGroup> + + <!-- Verify Simple Product 2 is in checkout cart items --> + <comment userInput="Verify Simple Product 2 is in checkout cart items" stepKey="commentVerifySimpleProduct2IsInCheckoutCartItems" after="guestCheckoutCheckSimpleProduct1InCartItems" /> + <actionGroup ref="CheckProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckSimpleProduct2InCartItems" after="commentVerifySimpleProduct2IsInCheckoutCartItems"> + <argument name="productVar" value="$$createSimpleProduct2$$"/> + </actionGroup> + <comment userInput="Place order with check money order payment" stepKey="commentPlaceOrderWithCheckMoneyOrderPayment" after="guestCheckoutCheckSimpleProduct2InCartItems" /> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" after="commentPlaceOrderWithCheckMoneyOrderPayment"/> <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="guestSeeBillingAddress" after="guestSelectCheckMoneyOrderPayment"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index 3c98f9177f4a7..fd6656b1d1b28 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -26,6 +26,8 @@ <field key="price">100.00</field> <requiredEntity createDataKey="createCategory"/> </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index 09a5ce4c70373..8b8aed3ac6204 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -15,9 +15,6 @@ <testCaseId value="MC-14720"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> - <skip> - <issueId value="MC-17140"/> - </skip> </annotations> <before> @@ -280,7 +277,9 @@ </actionGroup> <!-- Verify Product11 is not displayed in Mini Cart --> - <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="$$simpleProduct11.name$$" stepKey="dontSeeProduct11NameInMiniCart"/> - <dontSee selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="110" stepKey="dontSeeProduct11PriceInMiniCart"/> + <actionGroup ref="AssertStorefrontMiniCartProductDetailsAbsentActionGroup" stepKey="assertSimpleProduct11IsNotInMiniCart"> + <argument name="productName" value="$$simpleProduct11.name$$"/> + <argument name="productPrice" value="$110.00"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml index 40b781df9b2ae..4e19de659be26 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -148,7 +148,7 @@ <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> <deleteData createDataKey="multiple_address_customer" stepKey="deleteCustomer"/> @@ -221,6 +221,10 @@ <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue" /> <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="specificCountryValue" /> <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index a77341b8697b5..b6b9fe1e1a117 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -23,9 +23,13 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> @@ -112,7 +116,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 0" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml new file mode 100644 index 0000000000000..44bfe81b40dc0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Region updates after changing country "/> + <title value="Region updates after changing country "/> + <description value="Region dupdates after changing country and leaving region select unselected"/> + <severity value="CRITICAL"/> + <testCaseId value="https://github.com/magento/magento2/issues/23460"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login to storefront from customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBookPage"> + <argument name="menu" value="Address Book"/> + </actionGroup> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditAddress"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="{{updateCustomerFranceAddress.country}}" stepKey="selectCountry"/> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{updateCustomerFranceAddress.country}}" stepKey="seeAssertCustomerDefaultShippingAddressCountry"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php index daabb080b1c9a..82384fa83ab94 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php @@ -48,6 +48,9 @@ public function testProcess($cartData, $expected) $this->assertEquals($this->requestProcessor->process($cartData), $expected); } + /** + * @return array + */ public function cartDataProvider() { return [ diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 46dd8d9106545..021cd930c74c0 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> <action name="checkout/cart/add"> <section name="cart"/> + <section name="directory-data"/> </action> <action name="checkout/cart/delete"> <section name="cart"/> diff --git a/app/code/Magento/Checkout/view/frontend/templates/button.phtml b/app/code/Magento/Checkout/view/frontend/templates/button.phtml index b0087794ea850..6d1f076e6b26d 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/button.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/button.phtml @@ -7,7 +7,10 @@ <?php /** @var $block \Magento\Checkout\Block\Onepage\Success */ ?> <?php if ($block->getCanViewOrder() && $block->getCanPrintOrder()) :?> - <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" target="_blank" class="print"> + <a href="<?= $block->escapeUrl($block->getPrintUrl()) ?>" + class="action print" + target="_blank" + rel="noopener"> <?= $block->escapeHtml(__('Print receipt')) ?> </a> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 9b20a782c38d9..6e1b031ab48ce 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -9,8 +9,9 @@ define([ 'jquery', 'Magento_Checkout/js/model/new-customer-address', 'Magento_Customer/js/customer-data', - 'mage/utils/objects' -], function ($, address, customerData, mageUtils) { + 'mage/utils/objects', + 'underscore' +], function ($, address, customerData, mageUtils, _) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -18,6 +19,7 @@ define([ return { /** * Convert address form data to Address object + * * @param {Object} formData * @returns {Object} */ @@ -59,13 +61,15 @@ define([ delete addressData['region_id']; if (addressData['custom_attributes']) { - addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) - .map(function (customAttribute) { + addressData['custom_attributes'] = _.map( + addressData['custom_attributes'], + function (value, key) { return { - 'attribute_code': customAttribute[0], - 'value': customAttribute[1] + 'attribute_code': key, + 'value': value }; - }); + } + ); } return address(addressData); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 54e496131972e..fd12eed76ed50 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -13,8 +13,8 @@ define([ ], function (quote, defaultProcessor, totalsDefaultProvider, shippingService, cartCache, customerData) { 'use strict'; - var rateProcessors = [], - totalsProcessors = [], + var rateProcessors = {}, + totalsProcessors = {}, /** * Estimate totals for shipping address and update shipping rates. diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js index 7eddc0d1a58d4..be2199961e07a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-service.js @@ -10,7 +10,7 @@ define([ ], function (quote, defaultProcessor, customerAddressProcessor) { 'use strict'; - var processors = []; + var processors = {}; processors.default = defaultProcessor; processors['customer-address'] = customerAddressProcessor; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js index d506f0a4359c5..cf26f682ad3aa 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor.js @@ -11,7 +11,7 @@ define([ ], function (defaultProcessor) { 'use strict'; - var processors = []; + var processors = {}; processors['default'] = defaultProcessor; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index a9cbb1194cfd3..6d54f607484b4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -162,6 +162,9 @@ define([ this._clearError(); this._checkRegionRequired(country); + $(regionList).find('option:selected').removeAttr('selected'); + regionInput.val(''); + // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 59d1daa757138..e728a5c0fcdd5 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -159,7 +159,6 @@ function ( } addressData['save_in_address_book'] = this.saveInAddressBook() ? 1 : 0; newBillingAddress = createBillingAddress(addressData); - // New address must be selected as a billing address selectBillingAddress(newBillingAddress); checkoutData.setSelectedBillingAddress(newBillingAddress.getKey()); @@ -237,6 +236,30 @@ function ( */ getCode: function (parent) { return _.isFunction(parent.getCode) ? parent.getCode() : 'shared'; + }, + + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index d152f94397730..4adc1cd88c0ae 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -103,8 +103,8 @@ define([ }); if ( - cartData().website_id !== window.checkout.websiteId && - cartData().website_id !== undefined + cartData().website_id !== window.checkout.websiteId && cartData().website_id !== undefined || + cartData().storeId !== window.checkout.storeId && cartData().storeId !== undefined ) { customerData.reload(['cart'], false); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js index 54381ad96b0b9..939a2af1a25aa 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js @@ -7,12 +7,13 @@ define([ 'jquery', 'ko', 'uiComponent', + 'underscore', 'Magento_Checkout/js/action/select-shipping-address', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/shipping-address/form-popup-state', 'Magento_Checkout/js/checkout-data', 'Magento_Customer/js/customer-data' -], function ($, ko, Component, selectShippingAddressAction, quote, formPopUpState, checkoutData, customerData) { +], function ($, ko, Component, _, selectShippingAddressAction, quote, formPopUpState, checkoutData, customerData) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -47,6 +48,30 @@ define([ return countryData()[countryId] != undefined ? countryData()[countryId].name : ''; //eslint-disable-line }, + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; + }, + /** Set selected customer shipping address */ selectAddress: function () { selectShippingAddressAction(this.address()); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js index 4f4fc3de3e1a5..2bdfd063cb6fb 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/list.js @@ -16,7 +16,8 @@ define([ var defaultRendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - component: 'Magento_Checkout/js/view/shipping-address/address-renderer/default' + component: 'Magento_Checkout/js/view/shipping-address/address-renderer/default', + provider: 'checkoutProvider' }; return Component.extend({ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js index acc9f1c2391d9..009178cbb19b9 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js @@ -5,8 +5,9 @@ define([ 'uiComponent', + 'underscore', 'Magento_Customer/js/customer-data' -], function (Component, customerData) { +], function (Component, _, customerData) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -22,6 +23,30 @@ define([ */ getCountryName: function (countryId) { return countryData()[countryId] != undefined ? countryData()[countryId].name : ''; //eslint-disable-line + }, + + /** + * Get customer attribute label + * + * @param {*} attribute + * @returns {*} + */ + getCustomAttributeLabel: function (attribute) { + var resultAttribute; + + if (typeof attribute === 'string') { + return attribute; + } + + if (attribute.label) { + return attribute.label; + } + + resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { + value: attribute.value + }); + + return resultAttribute && resultAttribute.label || attribute.value; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js index 28eb83c8be3e3..3bb2715c78a7b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/list.js @@ -16,7 +16,8 @@ define([ var defaultRendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - component: 'Magento_Checkout/js/view/shipping-information/address-renderer/default' + component: 'Magento_Checkout/js/view/shipping-information/address-renderer/default', + provider: 'checkoutProvider' }; return Component.extend({ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index c811d3a1e8369..d5098dbc6f734 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -249,6 +249,16 @@ define([ if (this.validateShippingInformation()) { quote.billingAddress(null); checkoutDataResolver.resolveBillingAddress(); + registry.async('checkoutProvider')(function (checkoutProvider) { + var shippingAddressData = checkoutData.getShippingAddressFromData(); + + if (shippingAddressData) { + checkoutProvider.set( + 'shippingAddress', + $.extend(true, {}, checkoutProvider.get('shippingAddress'), shippingAddressData) + ); + } + }); setShippingInformationAction().done( function () { stepNavigator.next(); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index a0827d17d6622..23bbce48fee2c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -13,19 +13,8 @@ <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> <each args="data: currentBillingAddress().customAttributes, as: 'element'"> - <if args="typeof element === 'object'"> - <if args="element.label"> - <text args="element.label"/> - </if> - <ifnot args="element.label"> - <if args="element.value"> - <text args="element.value"/> - </if> - </ifnot> - </if> - <if args="typeof element === 'string'"> - <text args="element"/> - </if><br/> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> <button visible="!isAddressSameAsShipping()" diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html index cf64c0140b955..b14f4da3f5f7d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html @@ -13,21 +13,8 @@ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> <each args="data: address().customAttributes, as: 'element'"> - <each args="data: Object.keys(element), as: 'attribute'"> - <if args="typeof element[attribute] === 'object'"> - <if args="element[attribute].label"> - <text args="element[attribute].label"/> - </if> - <ifnot args="element[attribute].label"> - <if args="element[attribute].value"> - <text args="element[attribute].value"/> - </if> - </ifnot> - </if> - <if args="typeof element[attribute] === 'string'"> - <text args="element[attribute]"/> - </if><br/> - </each> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> <button visible="address().isEditable()" type="button" diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html index 75e061426d816..26dd7742d1da8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html @@ -13,18 +13,7 @@ <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> <each args="data: address().customAttributes, as: 'element'"> - <if args="typeof element === 'object'"> - <if args="element.label"> - <text args="element.label"/> - </if> - <ifnot args="element.label"> - <if args="element.value"> - <text args="element.value"/> - </if> - </ifnot> - </if> - <if args="typeof element === 'string'"> - <text args="element"/> - </if><br/> + <text args="$parent.getCustomAttributeLabel(element)"/> + <br/> </each> </if> diff --git a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml index 09cd1c5b63965..43da8d7d27dd9 100644 --- a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml +++ b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="checkout_agreement" resource="default" engine="innodb" comment="Checkout Agreement"> <column xsi:type="int" name="agreement_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Agreement Id"/> + comment="Agreement ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> <column xsi:type="text" name="content" nullable="true" comment="Content"/> <column xsi:type="varchar" name="content_height" nullable="true" length="25" comment="Content Height"/> @@ -26,9 +26,9 @@ </table> <table name="checkout_agreement_store" resource="default" engine="innodb" comment="Checkout Agreement Store"> <column xsi:type="int" name="agreement_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Agreement Id"/> + comment="Agreement ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> <column name="store_id"/> diff --git a/app/code/Magento/Cms/Model/BlockSearchResults.php b/app/code/Magento/Cms/Model/BlockSearchResults.php new file mode 100644 index 0000000000000..2fa5dbb40139e --- /dev/null +++ b/app/code/Magento/Cms/Model/BlockSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model; + +use Magento\Cms\Api\Data\BlockSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Block search results. + */ +class BlockSearchResults extends SearchResults implements BlockSearchResultsInterface +{ +} diff --git a/app/code/Magento/Cms/Model/PageSearchResults.php b/app/code/Magento/Cms/Model/PageSearchResults.php new file mode 100644 index 0000000000000..7985e382be273 --- /dev/null +++ b/app/code/Magento/Cms/Model/PageSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model; + +use Magento\Cms\Api\Data\PageSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Page search results. + */ +class PageSearchResults extends SearchResults implements PageSearchResultsInterface +{ +} diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml new file mode 100644 index 0000000000000..711a8af38efe3 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCMSBlockContentActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddImageToCMSBlockContent"> + <arguments> + <argument name="image" type="entity" defaultValue="MagentoLogo"/> + </arguments> + <click selector="{{TinyMCESection.InsertImage}}" stepKey="clickAddImageButton"/> + <waitForElementVisible selector="{{MediaGallerySection.Browse}}" stepKey="waitForBrowseImage"/> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowseImage"/> + <waitForElementVisible selector="{{MediaGallerySection.StorageRootArrow}}" stepKey="waitForAttacheFiles"/> + <waitForLoadingMaskToDisappear stepKey="waitForStorageRootLoadingMaskDisappear"/> + <click selector="{{MediaGallerySection.StorageRootArrow}}" stepKey="clickRoot"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <attachFile selector="{{MediaGallerySection.BrowseUploadImage}}" userInput="{{image.file}}" stepKey="attachLogo"/> + <waitForElementVisible selector="{{MediaGallerySection.InsertFile}}" stepKey="waitForAddSelected"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + <click selector="{{MediaGallerySection.InsertFile}}" stepKey="clickAddSelected"/> + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOk"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml index 2efa7f62fc4ec..445279a8b1403 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml @@ -24,6 +24,8 @@ </section> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> + <element name="image" type="file" selector="#tinymce img"/> + <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> <section name="CmsBlockBlockActionSection"> <element name="deleteBlock" type="button" selector="#delete" timeout="30"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml index fccc5b5980f2b..b7c7e4a4212fe 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsPageTest.xml @@ -23,7 +23,7 @@ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml new file mode 100644 index 0000000000000..bc1688c9d692b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryPopupUploadImagesWithoutErrorTest"> + <annotations> + <features value="Cms"/> + <stories value="Spinner is Always Displayed on Media Gallery popup"/> + <title value="Media Gallery popup upload images without error"/> + <description value="Media Gallery popup upload images without error"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-18962"/> + <useCaseId value="MC-18709"/> + <group value="Cms"/> + </annotations> + <before> + <!--Enable WYSIWYG options--> + <comment userInput="Enable WYSIWYG options" stepKey="commentEnableWYSIWYG"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYGEditor"/> + <magentoCLI command="config:set cms/wysiwyg/editor 'TinyMCE 4'" stepKey="setValueWYSIWYGEditor"/> + <!--Create block--> + <comment userInput="Create block" stepKey="commentCreateBlock"/> + <createData entity="Sales25offBlock" stepKey="createBlock"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + <after> + <!--Disable WYSIWYG options--> + <comment userInput="Disable WYSIWYG options" stepKey="commentDisableWYSIWYG"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createBlock" stepKey="deleteBlock" /> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open created block page and add image--> + <comment userInput="Open create block page and add image" stepKey="commentOpenBlockPage"/> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$createBlock$$"/> + </actionGroup> + <actionGroup ref="AdminAddImageToCMSBlockContent" stepKey="addImage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <click selector="{{BlockWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtnFirstTime"/> + <click selector="{{BlockWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtnSecondTime"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Switch to content frame and click on image--> + <comment userInput="Switch to content frame and click on image" stepKey="commentSwitchToIframe"/> + <switchToIFrame selector="{{BlockContentSection.contentIframe}}" stepKey="switchToContentFrame"/> + <click selector="{{BlockContentSection.image}}" stepKey="clickImage"/> + <switchToIFrame stepKey="switchBack"/> + <!--Add image second time and assert--> + <comment userInput="Add image second time and assert" stepKey="commentAddImageAndAssert"/> + <actionGroup ref="AdminAddImageToCMSBlockContent" stepKey="addImageSecondTime"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <switchToIFrame selector="{{BlockContentSection.contentIframe}}" stepKey="switchToContentFrameSecondTime"/> + <seeElement selector="{{BlockContentSection.image}}" stepKey="seeImageElement"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index e41f500915916..d82dc4ea86239 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -7,9 +7,9 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Cms\Api\Data\PageSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\PageSearchResults" /> <preference for="Magento\Cms\Api\Data\BlockSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Cms\Model\BlockSearchResults" /> <preference for="Magento\Cms\Api\GetBlockByIdentifierInterface" type="Magento\Cms\Model\GetBlockByIdentifier" /> <preference for="Magento\Cms\Api\GetPageByIdentifierInterface" type="Magento\Cms\Model\GetPageByIdentifier" /> <preference for="Magento\Cms\Api\Data\PageInterface" type="Magento\Cms\Model\Page" /> diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php index 62d6531978d8a..80ce061a0a17e 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php +++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php @@ -48,6 +48,9 @@ public function __construct( * Magic method called during class serialization * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -59,6 +62,9 @@ public function __sleep() * Magic method called during class un-serialization * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php new file mode 100644 index 0000000000000..8716fe5a23ad3 --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Config\Model\Config\Backend\File; + +/** + * System config PDF field backend model. + */ +class Pdf extends \Magento\Config\Model\Config\Backend\File +{ + /** + * @inheritdoc + */ + protected function _getAllowedExtensions() + { + return ['pdf']; + } +} diff --git a/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php b/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php index 44131fe8a7966..a5a81a4dde75d 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php +++ b/app/code/Magento/Config/Model/Config/Backend/Image/Pdf.php @@ -4,24 +4,24 @@ * See COPYING.txt for license details. */ -/** - * System config image field backend model for Zend PDF generator - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Config\Model\Config\Backend\Image; /** + * System config PDF field backend model. + * * @api * @since 100.0.2 + * @see \Magento\Config\Model\Config\Backend\File\Pdf */ class Pdf extends \Magento\Config\Model\Config\Backend\Image { /** + * Returns the list of allowed file extensions. + * * @return string[] */ protected function _getAllowedExtensions() { - return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg']; + return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg', 'pdf']; } } diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml index 10d22b61ecae0..2a3a14293a059 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SetGroupForValidVATIdIntraUnionActionGroup"> <annotations> - <description>Goes to the 'Configuration' page for 'Customer Configuration'. Sets the 'Group For Valid VAT ID Intra Union' option. Clicks on the Save button. Validates the the Save message is present.</description> + <description>Goes to the 'Configuration' page for 'Customer Configuration'. Sets the 'Group For Valid VAT ID Intra Union' option. Clicks on the Save button. Validates the Save message is present.</description> </annotations> <arguments> <argument name="value" type="string"/> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml new file mode 100644 index 0000000000000..79505e0627865 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminExpandConfigTabActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page and expands main level configuration tab passed via argument as Tab Name.</description> + </annotations> + <arguments> + <argument name="tabName" type="string"/> + </arguments> + + <scrollTo stepKey="scrollToTab" selector="{{AdminConfigSection.collapsibleTabByTitle(tabName)}}" x="0" y="-80"/> + <conditionalClick selector="{{AdminConfigSection.collapsibleTabByTitle(tabName)}}" dependentSelector="{{AdminConfigSection.expandedTabByTitle(tabName)}}" visible="false" stepKey="expandTab" /> + <waitForElement selector="{{AdminConfigSection.expandedTabByTitle(tabName)}}" stepKey="waitOpenedTab" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml new file mode 100644 index 0000000000000..eaca27f86f49a --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenConfigNavItemActionGroup"> + <annotations> + <description>Clicks on config nav item selected by passed argument.</description> + </annotations> + <arguments> + <argument name="navItem" type="string"/> + </arguments> + + <scrollTo stepKey="scrollToNavItem" selector="{{AdminConfigSection.navItemByTitle(navItem)}}" x="0" y="-80"/> + <click selector="{{AdminConfigSection.navItemByTitle(navItem)}}" stepKey="openNavItem" /> + <waitForElement selector="{{AdminConfigSection.activeNavItemByTitle(navItem)}}" stepKey="waitActiveNavItem" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml new file mode 100644 index 0000000000000..4c5d21a890973 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStoreConfigDeveloperPageActionGroup"> + <annotations> + <description>Go to admin store configuration developer page.</description> + </annotations> + + <amOnPage url="{{AdminConfigDeveloperPage.url}}" stepKey="openAdminStoreConfigDeveloperPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml new file mode 100644 index 0000000000000..43343fd0851e4 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStoreConfigPageActionGroup"> + <annotations> + <description>Go to admin store configuration page.</description> + </annotations> + + <amOnPage url="{{AdminConfigPage.url}}" stepKey="openAdminStoreConfigPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml index 45d84a338a30b..d65376828e2c4 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml @@ -27,7 +27,6 @@ <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToConfigGeneralPage"/> <waitForPageLoad stepKey="waitForConfigPageLoad"/> </actionGroup> - <actionGroup name="SelectTopDestinationsCountry"> <annotations> <description>Selects the provided Countries under 'Top destinations' on the 'General' section of the 'Configuration' page. Clicks on the Save button.</description> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml index 7a62dfff8323b..8fafdc202bf09 100644 --- a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml @@ -21,4 +21,7 @@ <page name="AdminConfigGeneralPage" url="admin/system_config/edit/section/general/" area="admin" module="Magento_Config"> <section name="GeneralSection"/> </page> + <page name="AdminConfigDeveloperPage" url="admin/system_config/edit/section/dev/" area="admin" module="Magento_Config"> + <section name="AdminConfigSection" /> + </page> </pages> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml index fd49c1482c133..ffe3f0076ca8d 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -7,6 +7,11 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminConfigSection"> + <element name="collapsibleTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="expandedTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][@aria-expanded='true'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="notExpandedTabByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@data-role='title'][@aria-expanded='false'][contains(.,'{{tabTitle}}')]" parameterized="true" /> + <element name="navItemByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@role='tablist']//li[contains(@class, 'nav-item')][contains(.,'{{navItem}}')]" parameterized="true" /> + <element name="activeNavItemByTitle" type="button" selector="//div[@id='system_config_tabs']//div[@role='tablist']//li[contains(@class, 'nav-item')][contains(@class, '_active')][contains(.,'{{navItem}}')]" parameterized="true" /> <element name="saveButton" type="button" selector="#save"/> <element name="generalTab" type="text" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='General']"/> <element name="generalTabClosed" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='false' or @aria-expanded='0']//strong[text()='General']"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml similarity index 55% rename from app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml rename to app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml index 621f2e6a67688..762d17bdf87f1 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml +++ b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml @@ -6,11 +6,14 @@ */ --> <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> - <suite name="PaypalTestSuite"> + <suite name="AppConfigDumpSuite"> + <before> + <magentoCLI command="app:config:dump" stepKey="configDump"/> + </before> + <after> + </after> <include> - <test name="CheckDefaultValueOfPayPalCustomizeButtonTest"/> - <test name="PayPalSmartButtonInCheckoutPage"/> - <test name="CheckCreditButtonConfiguration"/> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"/> </include> </suite> -</suites> \ No newline at end of file +</suites> diff --git a/app/code/Magento/Config/etc/db_schema.xml b/app/code/Magento/Config/etc/db_schema.xml index 8aeac802fbd91..46dd77959b9d4 100644 --- a/app/code/Magento/Config/etc/db_schema.xml +++ b/app/code/Magento/Config/etc/db_schema.xml @@ -9,10 +9,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="core_config_data" resource="default" engine="innodb" comment="Config Data"> <column xsi:type="int" name="config_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Config Id"/> + comment="Config ID"/> <column xsi:type="varchar" name="scope" nullable="false" length="8" default="default" comment="Config Scope"/> <column xsi:type="int" name="scope_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Config Scope Id"/> + default="0" comment="Config Scope ID"/> <column xsi:type="varchar" name="path" nullable="false" length="255" default="general" comment="Config Path"/> <column xsi:type="text" name="value" nullable="true" comment="Config Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Config/i18n/en_US.csv b/app/code/Magento/Config/i18n/en_US.csv index 9770bf4b94c27..ceb1efdc8b77d 100644 --- a/app/code/Magento/Config/i18n/en_US.csv +++ b/app/code/Magento/Config/i18n/en_US.csv @@ -118,3 +118,4 @@ Dashboard,Dashboard "Web Section","Web Section" "Store Email Addresses Section","Store Email Addresses Section" "Email to a Friend","Email to a Friend" +"Taiwan","Taiwan, Province of China" diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php index 4874dc8ea03ae..11384263b59a2 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php @@ -197,6 +197,7 @@ protected function getAttributes() foreach ($attributes as $key => $attribute) { if (isset($configurableData[$key])) { $attributes[$key] = array_replace_recursive($attribute, $configurableData[$key]); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $attributes[$key]['values'] = array_merge( isset($attribute['values']) ? $attribute['values'] : [], isset($configurableData[$key]['values']) @@ -412,14 +413,15 @@ private function prepareAttributes( 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], 'chosen' => [], ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { + $options = $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; + foreach ($options as $option) { + if (!empty($option['value'])) { $attributes[$attribute->getAttributeId()]['options'][] = [ 'attribute_code' => $attribute->getAttributeCode(), 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), + 'id' => $option['value'], + 'label' => $option['label'], + 'value' => $option['value'], '__disableTmpl' => true, ]; } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php index b013916cc221a..01549ffcd2755 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php @@ -113,11 +113,12 @@ public function afterSave() } /** - * Load configurable attribute by product and product's attribute + * Load configurable attribute by product and product's attribute. * * @param \Magento\Catalog\Model\Product $product * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @throws LocalizedException + * @return void */ public function loadByProductAndAttribute($product, $attribute) { @@ -263,6 +264,9 @@ public function setProductId($value) /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -274,6 +278,9 @@ public function __sleep() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php index 979587dc500a4..f837444aa45ca 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/VariationMatrix.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Model\Product\Type; /** + * Variation matrix. + * * @api * @since 100.0.2 */ @@ -40,7 +43,9 @@ public function getVariations($usedProductAttributes) for ($attributeIndex = $attributesCount; $attributeIndex--;) { $currentAttribute = $variationalAttributes[$attributeIndex]; $currentVariationValue = $currentVariation[$attributeIndex]; - $filledVariation[$currentAttribute['id']] = $currentAttribute['values'][$currentVariationValue]; + if (!empty($currentAttribute['id'])) { + $filledVariation[$currentAttribute['id']] = $currentAttribute['values'][$currentVariationValue]; + } } $variations[] = $filledVariation; diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php index 8f2cc6ddb43ce..3aa90c7b3ce57 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php @@ -302,7 +302,9 @@ protected function _loadLabels() } /** - * Load attribute options. + * Load related options' data. + * + * @return void */ protected function loadOptions() { @@ -355,6 +357,9 @@ protected function getIncludedOptions(array $usedProducts, AbstractAttribute $pr /** * @inheritdoc * @since 100.0.6 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -374,6 +379,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.6 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml index 3f21c98068d8a..6ae3c4f4e16cf 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -60,6 +60,19 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiConfigurableProductWithDescriptionUnderscoredSku" 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/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml index 9021bf981ac13..430007ae761f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml index 1a694b8adf17e..33a6da9dabf34 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest.xml @@ -66,7 +66,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> @@ -216,7 +216,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index c599a6a23f190..cd09cbd295877 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -52,7 +52,7 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!--Clean up category--> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create a configurable product with long name and sku--> @@ -87,6 +87,9 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductSku2"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> <waitForPageLoad stepKey="waitForStorefrontLoad"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml index ad30c91967c32..51b3e49f51913 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml @@ -77,7 +77,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> @@ -200,7 +200,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> @@ -301,7 +301,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml index 059a18200e90c..410c85d314904 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest.xml @@ -68,7 +68,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> @@ -147,7 +147,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml index 00ffe70380d18..dba481b64810a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml @@ -96,7 +96,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Delete everything that was created in the before block --> <deleteData createDataKey="createCategory" stepKey="deleteCatagory" /> @@ -213,7 +213,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Delete everything that was created in the before block --> <deleteData createDataKey="createCategory" stepKey="deleteCatagory" /> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml index 3a6a20de3ed90..5d5590011a852 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml @@ -37,7 +37,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> @@ -277,7 +277,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> @@ -326,7 +326,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a configurable product via the UI --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 925e7a890cead..9796c14f5519a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -129,6 +129,9 @@ <!-- Save product --> <actionGroup ref="saveConfigurableProductAddToCurrentAttributeSet" stepKey="saveProduct"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..fa21d20eb4456 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,181 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Simple product type switching on editing to configurable product"/> + <description value="Simple product type switching on editing to configurable product"/> + <testCaseId value="MAGETWO-29633"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!--Create attribute with options--> + <comment userInput="Create attribute with options" stepKey="commentCreateAttributeWithOptions"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add configurations to product--> + <comment userInput="Add configurations to product" stepKey="commentAddConfigs"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurations"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveConfigProductForm"/> + <!--Assert configurable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeProductNameInGrid1"/> + <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeProductNameInGrid2"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <!--Assert configurable product on storefront--> + <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="clickAttributeDropDown"/> + <see userInput="option1" stepKey="verifyOption1Exists"/> + <see userInput="option2" stepKey="verifyOption2Exists"/> + </test> + <test name="AdminConfigurableProductTypeSwitchingToVirtualProductTest" extends="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Configurable product type switching on editing to virtual product"/> + <description value="Configurable product type switching on editing to virtual product"/> + <testCaseId value="MC-17952"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!--Delete product configurations--> + <comment userInput="Delete product configuration" stepKey="commentDeleteConfigs"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToConfigProductPage"/> + <waitForPageLoad stepKey="waitForConfigurableProductPageLoad"/> + <conditionalClick selector="{{ AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandOption1Actions"/> + <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemoveOption1"/> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandOption2Actions"/> + <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemoveOption2"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{SimpleProduct2.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{SimpleProduct2.quantity}}" stepKey="fillProductQty"/> + <clearField selector="{{AdminProductFormSection.productWeight}}" stepKey="clearWeightField"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has no weight" stepKey="selectNoWeight"/> + <actionGroup ref="saveProductForm" stepKey="saveVirtualProductForm"/> + <!--Assert virtual product on Admin product page grid--> + <comment userInput="Assert virtual product on Admin product page grid" stepKey="commentAssertVirtualProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForVirtual"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySkuForVirtual"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeVirtualProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeVirtualProductTypeInGrid"/> + <!--Assert virtual product on storefront--> + <comment userInput="Assert virtual product on storefront" stepKey="commentAssertVirtualProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openVirtualProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontVirtualProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertVirtualProductInStock"/> + </test> + <test name="AdminVirtualProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product type switching"/> + <title value="Virtual product type switching on editing to configurable product"/> + <description value="Virtual product type switching on editing to configurable product"/> + <testCaseId value="MC-17953"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="VirtualProduct" stepKey="createProduct"/> + <!--Create attribute with options--> + <comment userInput="Create attribute with options" stepKey="commentCreateAttributeWithOptions"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add configurations to product--> + <comment userInput="Add configurations to product" stepKey="commentAddConfigurations"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToConfigProductPage"/> + <waitForPageLoad stepKey="waitForConfigurableProductPageLoad"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForConfigurableProduct"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurationsForProduct"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveNewConfigurableProductForm"/> + <!--Assert configurable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertConfigurableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPageForConfigurable"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySkuForConfigurable"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeConfigurableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeConfigurableProductTypeInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeConfigurableProductNameInGrid1"/> + <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeConfigurableProductNameInGrid2"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearConfigurableProductFilters"/> + <!--Assert configurable product on storefront--> + <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigurableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openConfigurableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontConfigurableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertConfigurableProductInStock"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="clickConfigurableAttributeDropDown"/> + <see userInput="option1" stepKey="verifyConfigurableProductOption1Exists"/> + <see userInput="option2" stepKey="verifyConfigurableProductOption2Exists"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml index c303e4d19db81..0370280309272 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml @@ -80,6 +80,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableBySkuTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> @@ -104,7 +106,7 @@ </createData> <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> - <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> + <createData entity="ApiConfigurableProductWithDescriptionUnderscoredSku" stepKey="product"/> <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> @@ -154,6 +156,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableByDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> @@ -228,6 +232,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> <test name="AdvanceCatalogSearchConfigurableByShortDescriptionTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> @@ -302,6 +308,8 @@ <after> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index a71f51526c8ab..83d9bbe8c270a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -77,7 +77,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 47ee09e4b2086..04687a2314dc6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -217,4 +217,213 @@ <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrcInComparison" after="compareAssertConfigProductInComparison"/> <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrcInComparison" stepKey="compareAssertConfigProductImageNotDefaultInComparison" after="compareGrabConfigProductImageSrcInComparison"/> </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <before> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildProduct1Image"> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigChildProduct2Image"> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateConfigProduct" createDataKey="createConfigProduct"/> + </before> + <after> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigChildProduct1Image" stepKey="deleteConfigChildProduct1Image"/>--> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigChildProduct2Image" stepKey="deleteConfigChildProduct2Image"/>--> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <!-- @TODO: Uncomment once MQE-679 is fixed --> + <!--<deleteData createDataKey="createConfigProductImage" stepKey="deleteConfigProductImage"/>--> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Verify Configurable Product in checkout cart items --> + <comment userInput="Verify Configurable Product in checkout cart items" stepKey="commentVerifyConfigurableProductInCheckoutCartItems" after="guestCheckoutCheckSimpleProduct2InCartItems" /> + <actionGroup ref="CheckConfigurableProductInCheckoutCartItemsActionGroup" stepKey="guestCheckoutCheckConfigurableProductInCartItems" after="commentVerifyConfigurableProductInCheckoutCartItems"> + <argument name="productVar" value="$$createConfigProduct$$"/> + <argument name="optionLabel" value="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$" /> + <argument name="optionValue" value="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" /> + </actionGroup> + + <!-- Check configurable product in category --> + <comment userInput="Verify Configurable Product in category" stepKey="commentVerifyConfigurableProductInCategory" after="browseAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="browseAssertCategoryConfigProduct" after="commentVerifyConfigurableProductInCategory"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="browseGrabConfigProductImageSrc" after="browseAssertCategoryConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$browseGrabConfigProductImageSrc" stepKey="browseAssertConfigProductImageNotDefault" after="browseGrabConfigProductImageSrc"/> + + <!-- View Configurable Product --> + <comment userInput="View Configurable Product" stepKey="commentViewConfigurableProduct" after="browseAssertSimpleProduct2PageImageNotDefault" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickCategory2" after="commentViewConfigurableProduct"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="browseClickCategoryConfigProductView" after="clickCategory2"/> + <waitForLoadingMaskToDisappear stepKey="waitForConfigurableProductViewloaded" after="browseClickCategoryConfigProductView"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="browseAssertConfigProductPage" after="waitForConfigurableProductViewloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="browseGrabConfigProductPageImageSrc" after="browseAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$browseGrabConfigProductPageImageSrc" stepKey="browseAssertConfigProductPageImageNotDefault" after="browseGrabConfigProductPageImageSrc"/> + + <!-- Add Configurable Product to cart --> + <comment userInput="Add Configurable Product to cart" stepKey="commentAddConfigurableProductToCart" after="cartAddProduct2ToCart" /> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory2" after="commentAddConfigurableProductToCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartCategory2loaded" after="cartClickCategory2"/> + <actionGroup ref="StorefrontCheckCategoryActionGroup" stepKey="cartAssertCategory1ForConfigurableProduct" after="waitForCartCategory2loaded"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="cartAssertConfigProduct" after="cartAssertCategory1ForConfigurableProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartGrabConfigProductImageSrc" after="cartAssertConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartGrabConfigProductImageSrc" stepKey="cartAssertConfigProductImageNotDefault" after="cartGrabConfigProductImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName($$createConfigProduct.name$$)}}" stepKey="cartClickCategoryConfigProductAddToCart" after="cartAssertConfigProductImageNotDefault"/> + <waitForElement selector="{{StorefrontMessagesSection.message('You need to choose options for your item.')}}" time="30" stepKey="cartWaitForConfigProductPageLoad" after="cartClickCategoryConfigProductAddToCart"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertConfigProductPage" after="cartWaitForConfigProductPageLoad"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabConfigProductPageImageSrc1" after="cartAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabConfigProductPageImageSrc1" stepKey="cartAssertConfigProductPageImageNotDefault1" after="cartGrabConfigProductPageImageSrc1"/> + <selectOption userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="cartConfigProductFillOption" after="cartAssertConfigProductPageImageNotDefault1"/> + <waitForLoadingMaskToDisappear stepKey="waitForConfigurableProductOptionloaded" after="cartConfigProductFillOption"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertConfigProductWithOptionPage" after="waitForConfigurableProductOptionloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartGrabConfigProductPageImageSrc2" after="cartAssertConfigProductWithOptionPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartGrabConfigProductPageImageSrc2" stepKey="cartAssertConfigProductPageImageNotDefault2" after="cartGrabConfigProductPageImageSrc2"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddConfigProductToCart" after="cartAssertConfigProductPageImageNotDefault2"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="productCount" value="3"/> + </actionGroup> + + <!-- Check configurable product in minicart --> + <comment userInput="Check configurable product in minicart" stepKey="commentCheckConfigurableProductInMinicart" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> + <actionGroup ref="StorefrontOpenMinicartAndCheckConfigurableProductActionGroup" stepKey="cartOpenMinicartAndCheckConfigProduct" after="commentCheckConfigurableProductInMinicart"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartMinicartGrabConfigProductImageSrc" after="cartOpenMinicartAndCheckConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/thumbnail\.jpg/'" actual="$cartMinicartGrabConfigProductImageSrc" stepKey="cartMinicartAssertConfigProductImageNotDefault" after="cartMinicartGrabConfigProductImageSrc"/> + <click selector="{{StorefrontMinicartSection.productOptionsDetailsByName($$createConfigProduct.name$$)}}" stepKey="cartMinicartClickConfigProductDetails" after="cartMinicartAssertConfigProductImageNotDefault"/> + <see userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{StorefrontMinicartSection.productOptionByNameAndAttribute($$createConfigProduct.name$$, $$createConfigProductAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="cartMinicartCheckConfigProductOption" after="cartMinicartClickConfigProductDetails"/> + <click selector="{{StorefrontMinicartSection.productLinkByName($$createConfigProduct.name$$)}}" stepKey="cartMinicartClickConfigProduct" after="cartMinicartCheckConfigProductOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForMinicartConfigProductloaded" after="cartMinicartClickConfigProduct"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertMinicartConfigProductPage" after="waitForMinicartConfigProductloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartMinicartGrabConfigProductPageImageSrc" after="cartAssertMinicartConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartMinicartGrabConfigProductPageImageSrc" stepKey="cartMinicartAssertConfigProductPageImageNotDefault" after="cartMinicartGrabConfigProductPageImageSrc"/> + + <!-- Check configurable product in cart --> + <comment userInput="Check configurable product in cart" stepKey="commentCheckConfigurableProductInCart" after="cartCartAssertSimpleProduct2PageImageNotDefault2" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart2" after="commentCheckConfigurableProductInCart"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="cartAssertCartConfigProduct" after="cartOpenCart2"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct2$$"/> + <!-- @TODO: Change to scalar value after MQE-498 is implemented --> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{CheckoutCartProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="cartCartGrabConfigProduct2ImageSrc" after="cartAssertCartConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$cartCartGrabConfigProduct2ImageSrc" stepKey="cartCartAssertConfigProduct2ImageNotDefault" after="cartCartGrabConfigProduct2ImageSrc"/> + <see userInput="$$createConfigProductAttributeOption2.option[store_labels][1][label]$$" selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute($$createConfigProduct.name$$, $$createConfigProductAttribute.attribute[frontend_labels][0][label]$$)}}" stepKey="cartCheckConfigProductOption" after="cartCartAssertConfigProduct2ImageNotDefault"/> + <click selector="{{CheckoutCartProductSection.ProductLinkByName($$createConfigProduct.name$$)}}" stepKey="cartClickCartConfigProduct" after="cartCheckConfigProductOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForCartConfigProductloaded" after="cartClickCartConfigProduct"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="cartAssertCartConfigProductPage" after="waitForCartConfigProductloaded"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="cartCartGrabConfigProductPageImageSrc" after="cartAssertCartConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$cartCartGrabConfigProductPageImageSrc" stepKey="cartCartAssertConfigProductPageImageNotDefault" after="cartCartGrabConfigProductPageImageSrc"/> + + <!-- Add Configurable Product to comparison --> + <comment userInput="Add Configurable Product to comparison" stepKey="commentAddConfigurableProductToComparison" after="compareAddSimpleProduct2ToCompare" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="compareAssertConfigProduct" after="commentAddConfigurableProductToComparison"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrc" after="compareAssertConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrc" stepKey="compareAssertConfigProductImageNotDefault" after="compareGrabConfigProductImageSrc"/> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="compareAddConfigProductToCompare" after="compareAssertConfigProductImageNotDefault"> + <argument name="productVar" value="$$createConfigProduct$$"/> + </actionGroup> + + <!-- Check configurable product in comparison sidebar --> + <comment userInput="Add Configurable Product in comparison sidebar" stepKey="commentAddConfigurableProductInComparisonSidebar" after="compareSimpleProduct2InSidebar" /> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="compareConfigProductInSidebar" after="commentAddConfigurableProductInComparisonSidebar"> + <argument name="productVar" value="$$createConfigProduct$$"/> + </actionGroup> + + <!-- Check configurable product on comparison page --> + <comment userInput="Add Configurable Product on comparison page" stepKey="commentAddConfigurableProductOnComparisonPage" after="compareAssertSimpleProduct2ImageNotDefaultInComparison" /> + <actionGroup ref="StorefrontCheckCompareConfigurableProductActionGroup" stepKey="compareAssertConfigProductInComparison" after="commentAddConfigurableProductOnComparisonPage"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductCompareMainSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="compareGrabConfigProductImageSrcInComparison" after="compareAssertConfigProductInComparison"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$compareGrabConfigProductImageSrcInComparison" stepKey="compareAssertConfigProductImageNotDefaultInComparison" after="compareGrabConfigProductImageSrcInComparison"/> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..a8e982475253f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search configurable product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search configurable product with product sku that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20389"/> + <group value="ConfigurableProduct"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="categoryHandle" before="simple1Handle"/> + + <createData entity="SimpleProduct" stepKey="simple1Handle" before="simple2Handle"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <createData entity="SimpleProduct" stepKey="simple2Handle" before="product"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <!-- TODO: Move configurable product creation to an actionGroup when MQE-697 is fixed --> + <createData entity="ApiConfigurableProductWithDescription" stepKey="product"/> + + <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> + + <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + + <createData entity="SimpleOne" stepKey="childProductHandle1"> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + </createData> + <createData entity="SimpleOne" stepKey="childProductHandle2"> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="childProductHandle1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="configProductHandle2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="childProductHandle2"/> + </createData> + </before> + <after> + <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2" before="delete"/> + <deleteData createDataKey="childProductHandle1" stepKey="deleteChildProduct1" before="delete"/> + <deleteData createDataKey="childProductHandle2" stepKey="deleteChildProduct2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml index ac468fc92e4db..805727e29a17a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml @@ -88,7 +88,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 1075f79aef187..16400fa837b1c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-249"/> <group value="ConfigurableProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> @@ -131,7 +132,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml index 836bc2cdca970..f75e30907a1f4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest.xml @@ -31,7 +31,7 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify configurable product details in storefront product view --> @@ -72,7 +72,7 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify configurable product options in storefront product view --> @@ -113,7 +113,7 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify adding configurable product to cart after an option is selected in storefront product view --> @@ -151,7 +151,7 @@ </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify not able to add configurable product to cart when no option is selected in storefront product view --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml index cc8291a83eb40..0ade410714a25 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest.xml @@ -32,7 +32,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify the storefront category grid view --> @@ -68,7 +68,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Verify storefront category list view --> @@ -106,7 +106,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Should be taken to product details page when adding to cart because an option needs to be selected --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index d890d59858116..4c955f3385643 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -25,7 +25,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index bb69122dc0be9..182c8c069ab23 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -135,6 +135,9 @@ <waitForPageLoad stepKey="waitForPageLoad1"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> <argument name="categoryName" value="$$createCategory.name$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php index 41995be418130..29bca356c1181 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/VariationMatrixTest.php @@ -25,46 +25,117 @@ protected function setUp() ); } - public function testGetVariations() + /** + * Variations matrix test. + * + * @param array $expectedResult + * @dataProvider variationProvider + */ + public function testGetVariations($expectedResult) { - $result = [ - [ - 130 => [ - 'value' => '3', - 'label' => 'red', - 'price' => ['value_index' => '3', 'pricing_value' => '', 'is_percent' => '0', 'include' => '1',], - ], - ], - [ - 130 => [ - 'value' => '4', - 'label' => 'blue', - 'price' => ['value_index' => '4', 'pricing_value' => '', 'is_percent' => '0', 'include' => '1',], - ], - ], - ]; - $input = [ - 130 => [ - 'values' => [ - [ - 'value_index' => '3', - 'pricing_value' => '', - 'is_percent' => '0', - 'include' => '1' - ], - [ - 'value_index' => '4', - 'pricing_value' => '', - 'is_percent' => '0', - 'include' => '1' + $this->assertEquals($expectedResult['result'], $this->model->getVariations($expectedResult['input'])); + } + + /** + * Test data provider. + */ + public function variationProvider() + { + return [ + [ + 'with_attribute_id' => [ + 'result' => [ + [ + 130 => [ + 'value' => '3', + 'label' => 'red', + 'price' => [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], + [ + 130 => [ + 'value' => '4', + 'label' => 'blue', + 'price' => [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], ], + 'input' => [ + 130 => [ + 'values' => [ + [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + 'attribute_id' => '130', + 'options' => [ + [ + 'value' => '3', + 'label' => 'red' + ], + ['value' => '4', + 'label' => 'blue' + ] + ], + ], + ] ], - 'attribute_id' => '130', - 'options' => [['value' => '3', 'label' => 'red',], ['value' => '4', 'label' => 'blue',],], - ], + 'without_attribute_id' => [ + 'result' => [ + [ + 130 => [ + 'value' => '4', + 'label' => 'blue', + 'price' => [ + 'value_index' => '4', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ], + ], + ], + ], + 'input' => [ + 130 => [ + 'values' => [ + [ + 'value_index' => '3', + 'pricing_value' => '', + 'is_percent' => '0', + 'include' => '1' + ] + ], + 'attribute_id' => '', + 'options' => [ + [ + 'value' => '3', + 'label' => 'red' + ] + ], + ], + ] + ] + ] ]; - - $this->assertEquals($result, $this->model->getVariations($input)); } } diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php index 7d337c57d7e77..055891ff79c69 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurableQty.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Catalog\Model\Locator\LocatorInterface; /** * Data provider for quantity in the Configurable products @@ -16,7 +19,22 @@ class ConfigurableQty extends AbstractModifier const CODE_QTY_CONTAINER = 'quantity_and_stock_status_qty'; /** - * {@inheritdoc} + * @var LocatorInterface + */ + private $locator; + + /** + * ConfigurableQty constructor + * + * @param LocatorInterface $locator + */ + public function __construct(LocatorInterface $locator) + { + $this->locator = $locator; + } + + /** + * @inheritdoc */ public function modifyData(array $data) { @@ -24,13 +42,14 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { if ($groupCode = $this->getGroupCodeByField($meta, self::CODE_QTY_CONTAINER)) { $parentChildren = &$meta[$groupCode]['children']; - if (!empty($parentChildren[self::CODE_QTY_CONTAINER])) { + $isConfigurable = $this->locator->getProduct()->getTypeId() === 'configurable'; + if (!empty($parentChildren[self::CODE_QTY_CONTAINER]) && $isConfigurable) { $parentChildren[self::CODE_QTY_CONTAINER] = array_replace_recursive( $parentChildren[self::CODE_QTY_CONTAINER], [ diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php index c474acbec5094..4bde97fa8022a 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php @@ -21,6 +21,8 @@ use Magento\Framework\Escaper; /** + * Associated products helper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AssociatedProducts @@ -231,6 +233,8 @@ public function getConfigurableAttributesData() * * @return void * @throws \Zend_Currency_Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ protected function prepareVariations() { @@ -262,14 +266,15 @@ protected function prepareVariations() 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], 'chosen' => [], ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { - $attributes[$attribute->getAttributeId()]['options'][$option->getValue()] = [ + $options = $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; + foreach ($options as $option) { + if (!empty($option['value'])) { + $attributes[$attribute->getAttributeId()]['options'][$option['value']] = [ 'attribute_code' => $attribute->getAttributeCode(), 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), + 'id' => $option['value'], + 'label' => $option['label'], + 'value' => $option['value'], ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php index f1971e228ac05..4a613254ddf84 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\Stdlib\ArrayManager; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; @@ -76,6 +77,10 @@ public function execute(array $cartItemData): array } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); } + $configurableProductLinks = $parentProduct->getExtensionAttributes()->getConfigurableProductLinks(); + if (!in_array($product->getId(), $configurableProductLinks)) { + throw new GraphQlInputException(__('Could not find specified product.')); + } $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $this->optionCollection->addProductId((int)$parentProduct->getData($linkField)); $options = $this->optionCollection->getAttributesByProductId((int)$parentProduct->getData($linkField)); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..4dfa09d77cec2 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableRegularPrice; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; + +/** + * Provides product prices for configurable products + */ +class Provider implements ProviderInterface +{ + /** + * @var ConfigurableOptionsProviderInterface + */ + private $optionsProvider; + + /** + * @var array + */ + private $minimumFinalAmounts = []; + + /** + * @var array + */ + private $maximumFinalAmounts = []; + + /** + * @param ConfigurableOptionsProviderInterface $optionsProvider + */ + public function __construct( + ConfigurableOptionsProviderInterface $optionsProvider + ) { + $this->optionsProvider = $optionsProvider; + } + + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + if (!isset($this->minimumFinalAmounts[$product->getId()])) { + $minimumAmount = null; + foreach ($this->optionsProvider->getProducts($product) as $variant) { + $variantAmount = $variant->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getAmount(); + if (!$minimumAmount || ($variantAmount->getValue() < $minimumAmount->getValue())) { + $minimumAmount = $variantAmount; + $this->minimumFinalAmounts[$product->getId()] = $variantAmount; + } + } + } + + return $this->minimumFinalAmounts[$product->getId()]; + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + /** @var ConfigurableRegularPrice $regularPrice */ + $regularPrice = $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE); + return $regularPrice->getMinRegularAmount(); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + if (!isset($this->maximumFinalAmounts[$product->getId()])) { + $maximumAmount = null; + foreach ($this->optionsProvider->getProducts($product) as $variant) { + $variantAmount = $variant->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getAmount(); + if (!$maximumAmount || ($variantAmount->getValue() > $maximumAmount->getValue())) { + $maximumAmount = $variantAmount; + $this->maximumFinalAmounts[$product->getId()] = $variantAmount; + } + } + } + + return $this->maximumFinalAmounts[$product->getId()]; + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + /** @var ConfigurableRegularPrice $regularPrice */ + $regularPrice = $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE); + return $regularPrice->getMaxRegularAmount(); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index 1d72524a31b76..f82bb0dbd4d91 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -29,4 +29,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="configurable" xsi:type="object">Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php b/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php new file mode 100644 index 0000000000000..9d8ae5b19be9c --- /dev/null +++ b/app/code/Magento/Cookie/Block/DataProviders/SessionConfig.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cookie\Block\DataProviders; + +use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provide cookie configuration + */ +class SessionConfig implements ArgumentInterface +{ + /** + * Session config + * + * @var ConfigInterface + */ + private $sessionConfig; + + /** + * Constructor + * + * @param ConfigInterface $sessionConfig + */ + public function __construct( + ConfigInterface $sessionConfig + ) { + $this->sessionConfig = $sessionConfig; + } + /** + * Get session.cookie_secure + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + public function getCookieSecure() + { + return $this->sessionConfig->getCookieSecure(); + } +} diff --git a/app/code/Magento/Cookie/view/adminhtml/layout/default.xml b/app/code/Magento/Cookie/view/adminhtml/layout/default.xml new file mode 100644 index 0000000000000..2862cf917856d --- /dev/null +++ b/app/code/Magento/Cookie/view/adminhtml/layout/default.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="after.body.start"> + <block class="Magento\Framework\View\Element\Js\Cookie" name="cookie_config" template="Magento_Cookie::html/cookie.phtml"> + <arguments> + <argument name="session_config" xsi:type="object">Magento\Cookie\Block\DataProviders\SessionConfig</argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/Cookie/view/base/requirejs-config.js b/app/code/Magento/Cookie/view/base/requirejs-config.js new file mode 100644 index 0000000000000..b4362ffd80cb6 --- /dev/null +++ b/app/code/Magento/Cookie/view/base/requirejs-config.js @@ -0,0 +1,10 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + paths: { + 'jquery/jquery-storageapi': 'Magento_Cookie/js/jquery.storageapi.extended' + } +}; diff --git a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml new file mode 100644 index 0000000000000..b05c53db02abf --- /dev/null +++ b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * Cookie settings initialization script + * + * @var $block \Magento\Framework\View\Element\Js\Cookie + */ +?> + +<script> + window.cookiesConfig = window.cookiesConfig || {}; + window.cookiesConfig.secure = <?= /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false' ?>; +</script> diff --git a/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js new file mode 100644 index 0000000000000..c026b205f0374 --- /dev/null +++ b/app/code/Magento/Cookie/view/base/web/js/jquery.storageapi.extended.js @@ -0,0 +1,69 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/jquery.cookie', + 'jquery/jquery.storageapi.min' +], function ($) { + 'use strict'; + + /** + * + * @param {Object} storage + * @private + */ + function _extend(storage) { + $.extend(storage, { + _secure: window.cookiesConfig ? window.cookiesConfig.secure : false, + + /** + * Set value under name + * @param {String} name + * @param {String} value + * @param {Object} [options] + */ + setItem: function (name, value, options) { + var _default = { + expires: this._expires, + path: this._path, + domain: this._domain, + secure: this._secure + }; + + $.cookie(this._prefix + name, value, $.extend(_default, options || {})); + }, + + /** + * Set default options + * @param {Object} c + * @returns {storage} + */ + setConf: function (c) { + if (c.path) { + this._path = c.path; + } + + if (c.domain) { + this._domain = c.domain; + } + + if (c.expires) { + this._expires = c.expires; + } + + if (typeof c.secure !== 'undefined') { + this._secure = c.secure; + } + + return this; + } + }); + } + + if (window.cookieStorage) { + _extend(window.cookieStorage); + } +}); diff --git a/app/code/Magento/Cookie/view/frontend/layout/default.xml b/app/code/Magento/Cookie/view/frontend/layout/default.xml index 8b6b86e81c51a..5202c624fefe1 100644 --- a/app/code/Magento/Cookie/view/frontend/layout/default.xml +++ b/app/code/Magento/Cookie/view/frontend/layout/default.xml @@ -9,6 +9,11 @@ <body> <referenceContainer name="after.body.start"> <block class="Magento\Cookie\Block\Html\Notices" name="cookie_notices" template="Magento_Cookie::html/notices.phtml"/> + <block class="Magento\Framework\View\Element\Js\Cookie" name="cookie_config" template="Magento_Cookie::html/cookie.phtml"> + <arguments> + <argument name="session_config" xsi:type="object">Magento\Cookie\Block\DataProviders\SessionConfig</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index b3061eefa6313..206b8f64f3ae7 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="cron_schedule" resource="default" engine="innodb" comment="Cron Schedule"> <column xsi:type="int" name="schedule_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Schedule Id"/> + comment="Schedule ID"/> <column xsi:type="varchar" name="job_code" nullable="false" length="255" default="0" comment="Job Code"/> <column xsi:type="varchar" name="status" nullable="false" length="7" default="pending" comment="Status"/> <column xsi:type="text" name="messages" nullable="true" comment="Messages"/> diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php index 34d24a8b0a7a8..a6e29db1e1c40 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -13,6 +12,9 @@ use Magento\Framework\Controller\ResultFactory; use Magento\CurrencySymbol\Controller\Adminhtml\System\Currency as CurrencyAction; +/** + * Class FetchRates + */ class FetchRates extends CurrencyAction implements HttpGetActionInterface, HttpPostActionInterface { /** @@ -41,20 +43,20 @@ public function execute() } $rates = $importModel->fetchRates(); $errors = $importModel->getMessages(); - if (sizeof($errors) > 0) { + if (count($errors) > 0) { foreach ($errors as $error) { - $this->messageManager->addWarning($error); + $this->messageManager->addWarningMessage($error); } - $this->messageManager->addWarning( + $this->messageManager->addWarningMessage( __('Click "Save" to apply the rates we found.') ); } else { - $this->messageManager->addSuccess(__('Click "Save" to apply the rates we found.')); + $this->messageManager->addSuccessMessage(__('Click "Save" to apply the rates we found.')); } $backendSession->setRates($rates); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php index 8dd6b5e6fac41..f5e1fdbdb0c56 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,6 +8,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +/** + * Class SaveRates + */ class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpPostActionInterface { /** @@ -23,12 +25,13 @@ public function execute() try { foreach ($data as $currencyCode => $rate) { foreach ($rate as $currencyTo => $value) { - $value = abs($this->_objectManager->get( - \Magento\Framework\Locale\FormatInterface::class - )->getNumber($value)); + $value = abs( + $this->_objectManager->get(\Magento\Framework\Locale\FormatInterface::class) + ->getNumber($value) + ); $data[$currencyCode][$currencyTo] = $value; if ($value == 0) { - $this->messageManager->addWarning( + $this->messageManager->addWarningMessage( __('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) ); } @@ -36,9 +39,9 @@ public function execute() } $this->_objectManager->create(\Magento\Directory\Model\Currency::class)->saveRates($data); - $this->messageManager->addSuccess(__('All valid rates have been saved.')); + $this->messageManager->addSuccessMessage(__('All valid rates have been saved.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php index 703117f34fce6..f77976cc9e2f2 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,9 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +/** + * Class Save + */ class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpPostActionInterface { /** @@ -29,9 +31,9 @@ public function execute() try { $this->_objectManager->create(\Magento\CurrencySymbol\Model\System\Currencysymbol::class) ->setCurrencySymbolsData($symbolsDataArray); - $this->messageManager->addSuccess(__('You applied the custom currency symbols.')); + $this->messageManager->addSuccessMessage(__('You applied the custom currency symbols.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml new file mode 100644 index 0000000000000..6b8a93ef3542d --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminCurrencyRatesActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetCurrencyRatesActionGroup"> + <arguments> + <argument name="firstCurrency" type="string" defaultValue="USD"/> + <argument name="secondCurrency" type="string" defaultValue="EUR"/> + <argument name="rate" type="string" defaultValue="0.5"/> + </arguments> + <fillField selector="{{AdminCurrencyRatesSection.currencyRate(firstCurrency, secondCurrency)}}" userInput="{{rate}}" stepKey="setCurrencyRate"/> + <click selector="{{AdminCurrencyRatesSection.saveCurrencyRates}}" stepKey="clickSaveCurrencyRates"/> + <waitForPageLoad stepKey="waitForSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="{{AdminSaveCurrencyRatesMessageData.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml new file mode 100644 index 0000000000000..61a6123b1a7af --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontCurrencyRatesActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchCurrency"> + <arguments> + <argument name="currency" type="string" defaultValue="EUR"/> + </arguments> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currencyTrigger}}" stepKey="openTrigger"/> + <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="waitForCurrency"/> + <click selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="chooseCurrency"/> + <see selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" userInput="{{currency}}" stepKey="seeSelectedCurrency"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.xml new file mode 100644 index 0000000000000..90d22b06fcb80 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminCurrencyRatesMessageData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminSaveCurrencyRatesMessageData"> + <data key="success">All valid rates have been saved.</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml new file mode 100644 index 0000000000000..6194287dd058b --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SetCurrencyUSDBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyEURBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForUSD"> + <data key="path">currency/options/allow</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForEUR"> + <data key="path">currency/options/allow</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForRUB"> + <data key="path">currency/options/allow</data> + <data key="value">RUB</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyEURConfig"> + <data key="path">currency/options/default</data> + <data key="value">EUR</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyUSDConfig"> + <data key="path">currency/options/default</data> + <data key="value">USD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.xml new file mode 100644 index 0000000000000..d31dd71d474bb --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/AdminCurrencyRatesPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCurrencyRatesPage" url="admin/system_currency/" area="admin" module="CurrencySymbol"> + <section name="AdminCurrencyRatesSection"/> + </page> +</pages> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml new file mode 100644 index 0000000000000..a5799356eb459 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCurrencyRatesSection"> + <element name="import" type="button" selector="//button[@title='Import']"/> + <element name="saveCurrencyRates" type="button" selector="//button[@title='Save Currency Rates']"/> + <element name="oldRate" type="text" selector="//div[contains(@class, 'admin__field-note') and contains(text(), 'Old rate:')]/strong"/> + <element name="currencyRate" type="input" selector="input[name='rate[{{fistCurrency}}][{{secondCurrency}}]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml new file mode 100644 index 0000000000000..e69823ad68e0e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontSwitchCurrencyRatesSection"> + <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> + <element name="currency" type="button" selector="//div[@id='switcher-currency-trigger']/following-sibling::ul//a[contains(text(), '{{currency}}')]" parameterized="true" timeout="10"/> + <element name="selectedCurrency" type="text" selector="#switcher-currency-trigger span"/> + </section> +</sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml new file mode 100644 index 0000000000000..26fbfd394be68 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminOrderRateDisplayWhenChooseThreeAllowedCurrenciesTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Currency rates order page"/> + <title value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct"/> + <description value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17255" /> + <useCaseId value="MAGETWO-67450"/> + <group value="currency"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createNewProduct"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}},{{SetAllowedCurrenciesConfigForRUB.value}}" stepKey="setAllowedCurrencyWebsitesEURandRUBandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyEURConfig.scope}} --scope-code={{SetDefaultCurrencyEURConfig.scope_code}} {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <!--Delete created product--> + <comment userInput="Delete created product" stepKey="commentDeleteCreatedProduct"/> + <deleteData createDataKey="createNewProduct" stepKey="deleteNewProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Set currency rates--> + <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="gotToCurrencyRatesPageSecondTime"/> + <waitForPageLoad stepKey="waitForLoadRatesPageSecondTime"/> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="setCurrencyRates"> + <argument name="firstCurrency" value="USD"/> + <argument name="secondCurrency" value="RUB"/> + <argument name="rate" value="0.8"/> + </actionGroup> + <!--Open created product on Storefront and place for order--> + <amOnPage url="{{StorefrontProductPage.url($$createNewProduct.custom_attributes[url_key]$$)}}" stepKey="goToNewProductPage"/> + <waitForPageLoad stepKey="waitForNewProductPagePageLoad"/> + <actionGroup ref="StorefrontSwitchCurrency" stepKey="switchCurrency"> + <argument name="currency" value="RUB"/> + </actionGroup> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontNewProductPage"> + <argument name="productName" value="$$createNewProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutNewProductFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutNewFillingShippingSection"> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectNewCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceNewOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabNewOrderNumber"/> + <!--Open order and check rates display in one line--> + <actionGroup ref="OpenOrderById" stepKey="openNewOrderById"> + <argument name="orderId" value="$grabNewOrderNumber"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="EUR / USD rate" stepKey="seeUSDandEURRate"/> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="RUB / USD rate:" stepKey="seeRUBandEURRate"/> + <grabMultiple selector="{{AdminOrderDetailsInformationSection.rate}}" stepKey="grabRates" /> + <assertEquals stepKey="assertRates"> + <actualResult type="variable">grabRates</actualResult> + <expectedResult type="array">['EUR / USD rate:', 'RUB / USD rate:']</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml new file mode 100644 index 0000000000000..dc6bdf3db542e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminOrderRateDisplayedInOneLineTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Currency rates order page"/> + <title value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct once"/> + <description value="Order rate converting currency for 'Base Currency' and 'Default Display Currency' displayed correct once"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17255" /> + <useCaseId value="MAGETWO-67450"/> + <group value="currency"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!--Set price scope website--> + <magentoCLI command="config:set {{CatalogPriceScopeWebsiteConfigData.path}} {{CatalogPriceScopeWebsiteConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyEURBaseConfig.path}} {{SetCurrencyEURBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyEURConfig.scope}} --scope-code={{SetDefaultCurrencyEURConfig.scope_code}} {{SetDefaultCurrencyEURConfig.path}} {{SetDefaultCurrencyEURConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <!--Delete created product--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Reset configurations--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeGlobal"/> + <magentoCLI command="config:set {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyUSDConfig.path}} {{SetDefaultCurrencyUSDConfig.value}}" stepKey="setCurrencyDefaultUSD"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}}" stepKey="setAllowedCurrencyUSD"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyUSDConfig.scope}} --scope-code={{SetDefaultCurrencyUSDConfig.scope_code}} {{SetDefaultCurrencyUSDConfig.path}} {{SetDefaultCurrencyUSDConfig.value}}" stepKey="setCurrencyDefaultUSDWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}}" stepKey="setAllowedCurrencyUSDWebsites"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open created product on Storefront and place for order--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPagePageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open order and check rates display in one line--> + <actionGroup ref="OpenOrderById" stepKey="openOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="EUR / USD rate" stepKey="seeEURandUSDRate"/> + <grabMultiple selector="{{AdminOrderDetailsInformationSection.rate}}" stepKey="grabRate" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[EUR / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php new file mode 100644 index 0000000000000..b561c02c7b36e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CurrencySymbol\Test\Unit\Controller\Adminhtml\System\Currency; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class SaveRatesTest + */ +class SaveRatesTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates + */ + protected $action; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $requestMock; + + /** + * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $responseMock; + + /** + * + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + + $this->responseMock = $this->createPartialMock( + \Magento\Framework\App\ResponseInterface::class, + ['setRedirect', 'sendResponse'] + ); + + $this->action = $objectManager->getObject( + \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates::class, + [ + 'request' => $this->requestMock, + 'response' => $this->responseMock, + ] + ); + } + + /** + * + */ + public function testWithNullRateExecute() + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('rate') + ->willReturn(null); + + $this->responseMock->expects($this->once())->method('setRedirect'); + + $this->action->execute(); + } +} diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 0863104a2bf8d..06f4294ce6397 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -128,7 +128,7 @@ public function testExecute() ->willReturn($this->filterManagerMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You applied the custom currency symbols.')); $this->action->execute(); diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php index db560f7de3ecb..3709f4914c477 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php @@ -75,7 +75,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _construct() { @@ -119,7 +119,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -201,7 +201,7 @@ public function getCustomerId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getGridUrl() { @@ -224,7 +224,13 @@ public function getGridParentHtml() */ public function getRowUrl($row) { - return $this->getUrl('catalog/product/edit', ['id' => $row->getProductId()]); + return $this->getUrl( + 'catalog/product/edit', + [ + 'id' => $row->getProductId(), + 'customerId' => $this->getCustomerId() + ] + ); } /** diff --git a/app/code/Magento/Customer/Block/SectionNamesProvider.php b/app/code/Magento/Customer/Block/SectionNamesProvider.php new file mode 100644 index 0000000000000..92029d1715d4b --- /dev/null +++ b/app/code/Magento/Customer/Block/SectionNamesProvider.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block; + +use Magento\Customer\CustomerData\SectionPool; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel to get sections names array. + */ +class SectionNamesProvider implements ArgumentInterface +{ + /** + * @var SectionPool + */ + private $sectionPool; + + /** + * @param SectionPool $sectionPool + */ + public function __construct( + SectionPool $sectionPool + ) { + $this->sectionPool = $sectionPool; + } + + /** + * Return array of section names based on config. + * + * @return array + */ + public function getSectionNames() + { + return $this->sectionPool->getSectionNames(); + } +} diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index d874729d9132e..e020de79a3a60 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -267,6 +267,8 @@ public function getHtmlExtraParams() $validators['validate-date'] = [ 'dateFormat' => $this->getDateFormat() ]; + $validators['validate-dob'] = true; + return 'data-validate="' . $this->_escaper->escapeHtml(json_encode($validators)) . '"'; } @@ -277,7 +279,11 @@ public function getHtmlExtraParams() */ public function getDateFormat() { - return $this->_localeDate->getDateFormatWithLongYear(); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); + /** Escape RTL characters which are present in some locales and corrupt formatting */ + $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + + return $escapedDateFormat; } /** diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index efea1762d9de6..eef2854cf363e 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -62,6 +62,16 @@ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = return $sectionsData; } + /** + * Return array of section names. + * + * @return array + */ + public function getSectionNames() + { + return array_keys($this->sectionSourceMap); + } + /** * Get section sources by section names * diff --git a/app/code/Magento/Customer/Model/AddressSearchResults.php b/app/code/Magento/Customer/Model/AddressSearchResults.php new file mode 100644 index 0000000000000..7e83aa8f8d8df --- /dev/null +++ b/app/code/Magento/Customer/Model/AddressSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\AddressSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Address search results. + */ +class AddressSearchResults extends SearchResults implements AddressSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/Attribute.php b/app/code/Magento/Customer/Model/Attribute.php index 98a97872f15f4..d05bf14fbc97d 100644 --- a/app/code/Magento/Customer/Model/Attribute.php +++ b/app/code/Magento/Customer/Model/Attribute.php @@ -202,6 +202,9 @@ public function canBeFilterableInGrid() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -214,6 +217,9 @@ public function __sleep() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Customer/Model/AttributeMetadataResolver.php b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php index 979730eb1c9c2..d548ef826a2e9 100644 --- a/app/code/Magento/Customer/Model/AttributeMetadataResolver.php +++ b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php @@ -16,6 +16,7 @@ use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share as ShareConfig; +use Magento\Customer\Model\FileUploaderDataResolver; /** * Class to build meta data of the customer or customer address attribute @@ -77,14 +78,14 @@ class AttributeMetadataResolver /** * @param CountryWithWebsites $countryWithWebsiteSource * @param EavValidationRules $eavValidationRules - * @param \Magento\Customer\Model\FileUploaderDataResolver $fileUploaderDataResolver + * @param FileUploaderDataResolver $fileUploaderDataResolver * @param ContextInterface $context * @param ShareConfig $shareConfig */ public function __construct( CountryWithWebsites $countryWithWebsiteSource, EavValidationRules $eavValidationRules, - fileUploaderDataResolver $fileUploaderDataResolver, + FileUploaderDataResolver $fileUploaderDataResolver, ContextInterface $context, ShareConfig $shareConfig ) { diff --git a/app/code/Magento/Customer/Model/CustomerSearchResults.php b/app/code/Magento/Customer/Model/CustomerSearchResults.php new file mode 100644 index 0000000000000..1d7f0e265641f --- /dev/null +++ b/app/code/Magento/Customer/Model/CustomerSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\CustomerSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Customer search results. + */ +class CustomerSearchResults extends SearchResults implements CustomerSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 573f86247e0c3..432317444f4b7 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -340,7 +340,7 @@ public function passwordReminder(CustomerInterface $customer) */ public function passwordResetConfirmation(CustomerInterface $customer) { - $storeId = $this->storeManager->getStore()->getId(); + $storeId = $customer->getStoreId(); if (!$storeId) { $storeId = $this->getWebsiteStoreId($customer); } diff --git a/app/code/Magento/Customer/Model/GroupSearchResults.php b/app/code/Magento/Customer/Model/GroupSearchResults.php new file mode 100644 index 0000000000000..1de4cd078db88 --- /dev/null +++ b/app/code/Magento/Customer/Model/GroupSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\GroupSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Customer Groups search results. + */ +class GroupSearchResults extends SearchResults implements GroupSearchResultsInterface +{ +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 529b0e806972a..03cf4b1bdddec 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -7,24 +7,25 @@ namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; -use Magento\Framework\Api\ExtensibleDataObjectConverter; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Customer\Model\Customer as CustomerModel; +use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\Data\CustomerSecureFactory; -use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\Delegation\Data\NewOperation; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\Api\Search\FilterGroup; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; -use Magento\Framework\Api\Search\FilterGroup; -use Magento\Framework\Event\ManagerInterface; -use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -203,7 +204,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $customer->setAddresses([]); $customerData = $this->extensibleDataObjectConverter->toNestedArray($customer, [], CustomerInterface::class); $customer->setAddresses($origAddresses); - /** @var Customer $customerModel */ + /** @var CustomerModel $customerModel */ $customerModel = $this->customerFactory->create(['data' => $customerData]); //Model's actual ID field maybe different than "id" so "id" field from $customerData may be ignored. $customerModel->setId($customer->getId()); diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml new file mode 100644 index 0000000000000..36c62a887c180 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StoreFrontClickEditDefaultShippingAddressActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> +<actionGroup name="StoreFrontClickEditDefaultShippingAddressActionGroup"> + <annotations> + <description>Click on the edit default shipping address link.</description> + </annotations> + + <click stepKey="ClickEditDefaultShippingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultShippingAddress}}"/> + <waitForPageLoad stepKey="waitForStorefrontSignInPageLoad"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml index b013b1db1c8e7..31a988ac9da0d 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml @@ -16,4 +16,15 @@ <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> <waitForPageLoad stepKey="waitForPageLoaded"/> </actionGroup> + + <actionGroup name="StorefrontOpenCustomerAccountCreatePageUsingStoreCodeInUrlActionGroup"> + <annotations> + <description>Goes to the Storefront Customer Create page using Store code in URL option.</description> + </annotations> + <arguments> + <argument name="storeView" type="string" defaultValue="{{customStore.code}}"/> + </arguments> + + <amOnPage url="{{StorefrontStoreHomePage.url(storeView)}}{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 4d7a39b3246e1..74365ef02fe87 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -51,6 +51,21 @@ <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionTX</requiredEntity> </entity> + <entity name="US_Address_TX_Without_Default" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="US_Address_TX_Default_Billing" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -239,6 +254,21 @@ <data key="state">Côtes-d'Armor</data> <data key="postcode">12345</data> </entity> + <entity name="updateCustomerChinaAddress" type="address"> + <data key="firstname">Xian</data> + <data key="lastname">Shai</data> + <data key="company">Hunan Fenmian</data> + <data key="telephone">+86 851 8410 4337</data> + <array key="street"> + <item>Nanyuan Rd, Wudang</item> + <item>Hunan Fenmian</item> + </array> + <data key="country_id">CN</data> + <data key="country">China</data> + <data key="city">Guiyang</data> + <data key="state">Guizhou Sheng</data> + <data key="postcode">550002</data> + </entity> <entity name="updateCustomerNoXSSInjection" type="address"> <data key="firstname">Jany</data> <data key="lastname">Doe</data> @@ -312,4 +342,37 @@ <data key="postcode">90230</data> <data key="telephone">555-55-555-55</data> </entity> + <entity name="US_Address_AE" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + <item>113</item> + </array> + <data key="city">Los Angeles</data> + <data key="state">Armed Forces Europe</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">90001</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionAE</requiredEntity> + </entity> + <entity name="updateCustomerBelgiumAddress" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>Chaussee de Wavre</item> + <item>318</item> + </array> + <data key="city">Bihain</data> + <data key="state">Hainaut</data> + <data key="country_id">BE</data> + <data key="country">Belgium</data> + <data key="postcode">6690</data> + <data key="telephone">0477-58-77867</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml new file mode 100644 index 0000000000000..e4c020cc449fb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminGeneralStoreInfomationConfigData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminGeneralSetVatNumberConfigData"> + <data key="path">general/store_information/merchant_vat_number</data> + <data key="value">111607872</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index c7a73b61dc48a..093d6a05e8c5c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -47,6 +47,20 @@ <data key="group">General</data> <requiredEntity type="address">US_Address_TX</requiredEntity> </entity> + <entity name="Simple_US_Customer_Without_Default_Address" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <data key="group">General</data> + <requiredEntity type="address">US_Address_TX_Without_Default</requiredEntity> + </entity> <entity name="SimpleUsCustomerWithNewCustomerGroup" type="customer"> <data key="default_billing">true</data> <data key="default_shipping">true</data> @@ -309,4 +323,17 @@ <data key="store_id">0</data> <data key="website_id">0</data> </entity> + <entity name="Simple_US_Customer_ArmedForcesEurope" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_AE</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml index 280bae7de411a..0a956f16767be 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml @@ -32,4 +32,9 @@ <data key="region_code">UT</data> <data key="region_id">58</data> </entity> + <entity name="RegionAE" type="region"> + <data key="region">Armed Forces Europe</data> + <data key="region_code">AFE</data> + <data key="region_id">9</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml index 9bd382da8eb92..79fb18afaad53 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -12,6 +12,8 @@ <section name="AdminCustomerAddressesGridSection"/> <section name="AdminCustomerAddressesGridActionsSection"/> <section name="AdminCustomerAddressesSection"/> + <section name="AdminCustomerCartSection" /> + <section name="AdminCustomerInformationSection" /> <section name="AdminCustomerMainActionsSection"/> <section name="AdminEditCustomerAddressesSection" /> </page> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml new file mode 100644 index 0000000000000..5c8b8907db43a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerCartSection"> + <element name="cartItem" type="button" selector="#customer_cart_grid_table tbody tr:nth-of-type({{row}}) .col-product_id" parameterized="true" timeout="5"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml new file mode 100644 index 0000000000000..d680015230b9d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerInformationSection"> + <element name="customerView" type="button" selector="#tab_customer_edit_tab_view_content"/> + <element name="accountInformation" type="button" selector="#tab_customer"/> + <element name="addresses" type="button" selector="#tab_address"/> + <element name="orders" type="button" selector="#tab_orders_content"/> + <element name="shoppingCart" type="button" selector="#tab_cart_content"/> + <element name="newsletter" type="button" selector="#tab_newsletter_content"/> + <element name="billingAgreements" type="button" selector="#tab_customer_edit_tab_agreements_content"/> + <element name="productReviews" type="button" selector="#tab_reviews_content"/> + <element name="wishList" type="button" selector="#tab_wishlist_content"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 78bae7ad60dd8..a11fb9d0eaa8f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -23,7 +23,7 @@ <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml new file mode 100644 index 0000000000000..d2d3343a3b8d3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Order"/> + <title value="Place an order and click print"/> + <description value="Admin panel is not frozen if Storefront is opened via Customer View"/> + <severity value="MAJOR"/> + <testCaseId value="https://github.com/magento/magento2/pull/24845"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="simpleCustomer"/> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createSimpleCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$simpleCustomer$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSecondProduct"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerInfo"> + <argument name="customer" value="$simpleCustomer$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRate"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> + + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipment"/> + <actionGroup ref="submitShipmentIntoOrder" stepKey="submitShipment"/> + + <!--Create Credit Memo--> + <actionGroup ref="StartToCreateCreditMemoActionGroup" stepKey="startToCreateCreditMemo"> + <argument name="orderId" value="{$getOrderId}"/> + </actionGroup> + <actionGroup ref="SubmitCreditMemoActionGroup" stepKey="submitCreditMemo"/> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInCustomer"> + <argument name="Customer" value="$$simpleCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToMyOrdersPage"> + <argument name="menu" value="My Orders"/> + </actionGroup> + <click selector="{{StorefrontCustomerOrderSection.viewOrder}}" stepKey="clickViewOrder"/> + <click selector="{{StorefrontCustomerOrderViewSection.printOrderLink}}" stepKey="clickPrintOrderLink"/> + <waitForPageLoad stepKey="waitPageReload"/> + <switchToWindow stepKey="switchToWindow"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToAddressBook"> + <argument name="menu" value="Address Book"/> + </actionGroup> + <see selector="{{CheckoutOrderSummarySection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}} {{US_Address_TX.city}}, {{US_Address_TX.state}}, {{US_Address_TX.postcode}}" stepKey="checkShippingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml new file mode 100644 index 0000000000000..9de2339f2e217 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductBackRedirectNavigateFromCustomerViewCartProduct"> + <annotations> + <features value="Customer"/> + <title value="Product back redirect navigate from customer view cart product"/> + <description value="Back button on product page is redirecting to customer page if opened form shopping cart"/> + <severity value="MINOR"/> + <group value="Customer"/> + </annotations> + <before> + <!-- Create new product--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Go to storefront as customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Navigate to customer edit page in admin --> + <amOnPage url="{{AdminCustomerPage.url}}edit/id/$$createCustomer.id$$/" stepKey="openCustomerEditPage"/> + <waitForPageLoad stepKey="waitForCustomerEditPage"/> + + <!-- Open shopping cart --> + <click selector="{{AdminCustomerInformationSection.shoppingCart}}" stepKey="clickShoppingCartButton"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + + <!-- Open product --> + <click selector="{{AdminCustomerCartSection.cartItem('1')}}" stepKey="openProduct"/> + + <!-- Go back to customer page --> + <click selector="{{AdminProductFormActionSection.backButton}}" stepKey="goBackToCustomerPage"/> + + <!-- Check current page is customer page --> + <seeInCurrentUrl stepKey="onCustomerAccountPage" url="{{AdminCustomerPage.url}}edit/id/$$createCustomer.id$$/"/> + + <after> + <!--Delete product--> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete category--> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + + <!--Delete customer--> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Sign out--> + <actionGroup ref="SignOut" stepKey="signOut"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 2b24233e8b072..bf8844b2cc7ab 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -25,7 +25,7 @@ <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Step 0: User signs up an account --> <comment userInput="Start of signing up user account" stepKey="startOfSigningUpUserAccount" /> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml index 413bbfd06a539..e2c55eb3962f2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml @@ -24,7 +24,7 @@ </before> <after> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Log in to Storefront as Customer 1 --> @@ -101,7 +101,7 @@ </before> <after> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Log in to Storefront as Customer 1 --> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml index 97c932f0cb28a..7d51f97f2463a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml @@ -20,7 +20,7 @@ <group value="create"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml new file mode 100644 index 0000000000000..6c0615f701df6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateCustomerAddressBelgiumTest"> + <annotations> + <stories value="Update Regions list for Belgium country"/> + <title value="Update customer address on storefront with Belgium address"/> + <description value="Update customer address on storefront with Belgium address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address Belgium in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml new file mode 100644 index 0000000000000..285de8d777b48 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateCustomerAddressChinaTest"> + <annotations> + <stories value="Update Regions list for China country"/> + <title value="Update customer address on storefront with china address"/> + <description value="Update customer address on storefront with china address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerChinaAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 8bfddac3cef8f..b1d7c455324b3 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -9,13 +9,13 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\ValidationRuleInterface; +use Magento\Customer\Block\Widget\Dob; use Magento\Customer\Helper\Address; use Magento\Framework\App\CacheInterface; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Data\Form\FilterFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Block\Widget\Dob; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime\Timezone; @@ -23,7 +23,7 @@ use Magento\Framework\View\Element\Html\Date; use Magento\Framework\View\Element\Template\Context; use PHPUnit\Framework\TestCase; -use PHPUnit_Framework_MockObject_MockObject; +use PHPUnit\Framework\MockObject\MockObject; use Zend_Cache_Backend_BlackHole; use Zend_Cache_Core; @@ -60,17 +60,17 @@ class DobTest extends TestCase const YEAR_HTML = '<div><label for="year"><span>yy</span></label><input type="text" id="year" name="Year" value="14"></div>'; - /** @var PHPUnit_Framework_MockObject_MockObject|AttributeMetadataInterface */ + /** @var MockObject|AttributeMetadataInterface */ protected $attribute; /** @var Dob */ protected $_block; - /** @var PHPUnit_Framework_MockObject_MockObject|CustomerMetadataInterface */ + /** @var MockObject|CustomerMetadataInterface */ protected $customerMetadata; /** - * @var FilterFactory|PHPUnit_Framework_MockObject_MockObject + * @var FilterFactory|MockObject */ protected $filterFactory; @@ -336,12 +336,27 @@ public function getYearDataProvider() } /** - * is used to derive the Locale that is used to determine the - * value of Dob::getDateFormat() for that Locale. + * Is used to derive the Locale that is used to determine the value of Dob::getDateFormat() for that Locale + * + * @param string $locale + * @param string $expectedFormat + * @dataProvider getDateFormatDataProvider + */ + public function testGetDateFormat(string $locale, string $expectedFormat) + { + $this->_locale = $locale; + $this->assertEquals($expectedFormat, $this->_block->getDateFormat()); + } + + /** + * @return array */ - public function testGetDateFormat() + public function getDateFormatDataProvider(): array { - $this->assertEquals(self::DATE_FORMAT, $this->_block->getDateFormat()); + return [ + ['ar_SA', 'd/M/y'], + [Resolver::DEFAULT_LOCALE, self::DATE_FORMAT], + ]; } /** @@ -521,8 +536,8 @@ public function testGetHtmlExtraParamsWithoutRequiredOption() { $this->escaper->expects($this->any()) ->method('escapeHtml') - ->with('{"validate-date":{"dateFormat":"M\/d\/Y"}}') - ->will($this->returnValue('{"validate-date":{"dateFormat":"M\/d\/Y"}}')); + ->with('{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}') + ->will($this->returnValue('{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}')); $this->attribute->expects($this->once()) ->method("isRequired") @@ -530,7 +545,7 @@ public function testGetHtmlExtraParamsWithoutRequiredOption() $this->assertEquals( $this->_block->getHtmlExtraParams(), - 'data-validate="{"validate-date":{"dateFormat":"M\/d\/Y"}}"' + 'data-validate="{"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}"' ); } @@ -544,13 +559,17 @@ public function testGetHtmlExtraParamsWithRequiredOption() ->willReturn(true); $this->escaper->expects($this->any()) ->method('escapeHtml') - ->with('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}') - ->will($this->returnValue('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}')); + ->with('{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}') + ->will( + $this->returnValue( + '{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}' + ) + ); $this->context->expects($this->any())->method('getEscaper')->will($this->returnValue($this->escaper)); $this->assertEquals( - 'data-validate="{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"}}"', + 'data-validate="{"required":true,"validate-date":{"dateFormat":"M\/d\/Y"},"validate-dob":true}"', $this->_block->getHtmlExtraParams() ); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php index 318023d8068c5..ff83ef62c6aa7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/EmailNotificationTest.php @@ -521,7 +521,7 @@ public function testPasswordResetConfirmation() /** @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ $customer = $this->createMock(CustomerInterface::class); - $customer->expects($this->any()) + $customer->expects($this->once()) ->method('getStoreId') ->willReturn($customerStoreId); $customer->expects($this->any()) @@ -539,11 +539,6 @@ public function testPasswordResetConfirmation() ->method('getStore') ->willReturn($this->storeMock); - $this->storeManagerMock->expects($this->at(1)) - ->method('getStore') - ->with($customerStoreId) - ->willReturn($this->storeMock); - $this->customerRegistryMock->expects($this->once()) ->method('retrieveSecureData') ->with($customerId) diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php index b245702ce07f9..069ddc63d74d7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php @@ -127,7 +127,7 @@ public function testSaveWithReservedId() ] ) ->getMockForAbstractClass(); - $dbAdapter->expects($this->any())->method('describeTable')->willReturn([]); + $dbAdapter->expects($this->any())->method('describeTable')->willReturn(['customer_group_id' => []]); $dbAdapter->expects($this->any())->method('update')->willReturnSelf(); $dbAdapter->expects($this->once())->method('lastInsertId')->willReturn($expectedId); $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml index c699db06d30dc..e07d7d8708a43 100644 --- a/app/code/Magento/Customer/etc/db_schema.xml +++ b/app/code/Magento/Customer/etc/db_schema.xml @@ -15,7 +15,7 @@ <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Group ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -78,7 +78,7 @@ <table name="customer_address_entity" resource="default" engine="innodb" comment="Customer Address Entity"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Parent ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -124,9 +124,9 @@ <table name="customer_address_entity_datetime" resource="default" engine="innodb" comment="Customer Address Entity Datetime"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> @@ -155,9 +155,9 @@ <table name="customer_address_entity_decimal" resource="default" engine="innodb" comment="Customer Address Entity Decimal"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" @@ -424,14 +424,14 @@ <column xsi:type="varchar" name="customer_group_code" nullable="false" length="32" comment="Customer Group Code"/> <column xsi:type="int" name="tax_class_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Tax Class Id"/> + default="0" comment="Tax Class ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="customer_group_id"/> </constraint> </table> <table name="customer_eav_attribute" resource="default" engine="innodb" comment="Customer Eav Attribute"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="is_visible" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Is Visible"/> <column xsi:type="varchar" name="input_filter" nullable="true" length="255" comment="Input Filter"/> @@ -461,7 +461,7 @@ <table name="customer_form_attribute" resource="default" engine="innodb" comment="Customer Form Attribute"> <column xsi:type="varchar" name="form_code" nullable="false" length="32" comment="Form Code"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="form_code"/> <column name="attribute_id"/> @@ -476,9 +476,9 @@ <table name="customer_eav_attribute_website" resource="default" engine="innodb" comment="Customer Eav Attribute Website"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="is_visible" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Visible"/> <column xsi:type="smallint" name="is_required" padding="5" unsigned="true" nullable="true" identity="false" @@ -504,7 +504,7 @@ <column xsi:type="bigint" name="visitor_id" padding="20" unsigned="true" nullable="false" identity="true" comment="Visitor ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="session_id" nullable="true" length="64" comment="Session ID"/> <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Last Visit Time"/> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index a181d6dd217fd..6086a61157ddc 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -28,11 +28,11 @@ <preference for="Magento\Customer\Api\Data\ValidationResultsInterface" type="Magento\Customer\Model\Data\ValidationResults" /> <preference for="Magento\Customer\Api\Data\GroupSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\GroupSearchResults" /> <preference for="Magento\Customer\Api\Data\CustomerSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\CustomerSearchResults" /> <preference for="Magento\Customer\Api\Data\AddressSearchResultsInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\Customer\Model\AddressSearchResults" /> <preference for="Magento\Customer\Api\AccountManagementInterface" type="Magento\Customer\Model\AccountManagement" /> <preference for="Magento\Customer\Api\CustomerMetadataInterface" @@ -157,6 +157,11 @@ <argument name="sectionConfig" xsi:type="object">SectionInvalidationConfigData</argument> </arguments> </type> + <type name="Magento\Customer\Block\SectionNamesProvider"> + <arguments> + <argument name="sectionConfig" xsi:type="object">SectionInvalidationConfigData</argument> + </arguments> + </type> <preference for="Magento\Customer\CustomerData\JsLayoutDataProviderPoolInterface" type="Magento\Customer\CustomerData\JsLayoutDataProviderPool"/> <type name="Magento\Framework\Webapi\ServiceTypeToEntityTypeMap"> diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index 3495feb925cb3..a70aa08dba735 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -539,3 +539,4 @@ Addresses,Addresses "Prefix","Prefix" "Middle Name/Initial","Middle Name/Initial" "Suffix","Suffix" +"The Date of Birth should not be greater than today.","The Date of Birth should not be greater than today." diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 5fb8b17dbb8c5..954b44ec19bbb 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -265,10 +265,20 @@ <settings> <validation> <rule name="validate-date" xsi:type="boolean">true</rule> + <rule name="validate-dob" xsi:type="boolean">true</rule> </validation> <dataType>text</dataType> <visible>true</visible> </settings> + <formElements> + <date> + <settings> + <options> + <option name="maxDate" xsi:type="string">-1d</option> + </options> + </settings> + </date> + </formElements> </field> <field name="taxvat" formElement="input"> <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml index 94e46fda194b0..3976fc6bd9090 100644 --- a/app/code/Magento/Customer/view/frontend/layout/default.xml +++ b/app/code/Magento/Customer/view/frontend/layout/default.xml @@ -41,9 +41,12 @@ </arguments> </block> <block name="customer.section.config" class="Magento\Customer\Block\SectionConfig" - template="Magento_Customer::js/section-config.phtml"/> - <block name="customer.customer.data" - class="Magento\Customer\Block\CustomerData" + template="Magento_Customer::js/section-config.phtml"> + <arguments> + <argument name="sectionNamesProvider" xsi:type="object">Magento\Customer\Block\SectionNamesProvider</argument> + </arguments> + </block> + <block name="customer.customer.data" class="Magento\Customer\Block\CustomerData" template="Magento_Customer::js/customer-data.phtml"/> <block name="customer.data.invalidation.rules" class="Magento\Customer\Block\CustomerScopeData" template="Magento_Customer::js/customer-data/invalidation-rules.phtml"/> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml index ebbd16164d7e8..e6511a0674e1d 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml @@ -15,7 +15,9 @@ "baseUrls": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode(array_unique([ $block->getUrl(null, ['_secure' => true]), $block->getUrl(null, ['_secure' => false]), - ])) ?> + ])) ?>, + "sectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class) + ->jsonEncode($block->getData('sectionNamesProvider')->getSectionNames()) ?> } } } diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index ac4b9f93e0c54..3c2f970faadee 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -35,3 +35,11 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; <?php endif; ?> </div> </div> + +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/validation": {} + } + } + </script> diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js index 41cf05df2b1d5..de3ff10bb057b 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js +++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js @@ -198,30 +198,9 @@ define([ * Customer data initialization */ init: function () { - var privateContentVersion = 'private_content_version', - privateContent = $.cookieStorage.get(privateContentVersion), - localPrivateContent = $.localStorage.get(privateContentVersion), - needVersion = 'need_version', - expiredSectionNames = this.getExpiredSectionNames(); - - if (privateContent && - !$.cookieStorage.isSet(privateContentVersion) && - !$.localStorage.isSet(privateContentVersion) - ) { - $.cookieStorage.set(privateContentVersion, needVersion); - $.localStorage.set(privateContentVersion, needVersion); - this.reload([], false); - } else if (localPrivateContent !== privateContent) { - if (!$.cookieStorage.isSet(privateContentVersion)) { - privateContent = needVersion; - $.cookieStorage.set(privateContentVersion, privateContent); - } - $.localStorage.set(privateContentVersion, privateContent); - _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { - buffer.notify(sectionName, sectionData); - }); - this.reload([], false); - } else if (expiredSectionNames.length > 0) { + var expiredSectionNames = this.getExpiredSectionNames(); + + if (expiredSectionNames.length > 0) { _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { buffer.notify(sectionName, sectionData); }); @@ -341,7 +320,9 @@ define([ var sectionDataIds, sectionsNamesForInvalidation; - sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? buffer.keys() : sectionNames; + sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? sectionConfig.getSectionNames() : + sectionNames; + $(document).trigger('customer-data-invalidate', [sectionsNamesForInvalidation]); buffer.remove(sectionsNamesForInvalidation); sectionDataIds = $.cookieStorage.get('section_data_ids') || {}; diff --git a/app/code/Magento/Customer/view/frontend/web/js/section-config.js b/app/code/Magento/Customer/view/frontend/web/js/section-config.js index 76fe7f2515a3a..d346d5b070729 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/section-config.js +++ b/app/code/Magento/Customer/view/frontend/web/js/section-config.js @@ -6,7 +6,7 @@ define(['underscore'], function (_) { 'use strict'; - var baseUrls, sections, clientSideSections, canonize; + var baseUrls, sections, clientSideSections, sectionNames, canonize; /** * @param {String} url @@ -70,6 +70,15 @@ define(['underscore'], function (_) { return _.contains(clientSideSections, sectionName); }, + /** + * Returns array of section names. + * + * @returns {Array} + */ + getSectionNames: function () { + return sectionNames; + }, + /** * @param {Object} options * @constructor @@ -78,6 +87,7 @@ define(['underscore'], function (_) { baseUrls = options.baseUrls; sections = options.sections; clientSideSections = options.clientSideSections; + sectionNames = options.sectionNames; } }; }); diff --git a/app/code/Magento/Customer/view/frontend/web/js/validation.js b/app/code/Magento/Customer/view/frontend/web/js/validation.js new file mode 100644 index 0000000000000..67a714212026a --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/validation.js @@ -0,0 +1,20 @@ +define([ + 'jquery', + 'moment', + 'jquery/validate', + 'mage/translate' +], function ($, moment) { + 'use strict'; + + $.validator.addMethod( + 'validate-dob', + function (value) { + if (value === '') { + return true; + } + + return moment(value).isBefore(moment()); + }, + $.mage.__('The Date of Birth should not be greater than today.') + ); +}); diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php index a4649bccc02e8..8741bff7aa88d 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -125,6 +125,8 @@ public function execute(AddressInterface $address): array } $addressData = array_merge($addressData, $customAttributes); + $addressData['customer_id'] = null; + return $addressData; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php index 3cc831e1ca40e..c252628b6566e 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php @@ -9,12 +9,8 @@ use Magento\Customer\Model\AuthenticationInterface; use Magento\Framework\Exception\InvalidEmailOrPasswordException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** * Check customer password @@ -41,8 +37,6 @@ public function __construct( * @param string $password * @param int $customerId * @throws GraphQlAuthenticationException - * @throws GraphQlInputException - * @throws GraphQlNoSuchEntityException */ public function execute(string $password, int $customerId) { @@ -52,10 +46,6 @@ public function execute(string $password, int $customerId) throw new GraphQlAuthenticationException(__($e->getMessage()), $e); } catch (UserLockedException $e) { throw new GraphQlAuthenticationException(__($e->getMessage()), $e); - } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); - } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php index de37482aca056..c62a931809644 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -101,8 +101,16 @@ public function execute(CustomerInterface $customer): array } } $customerData = array_merge($customerData, $customAttributes); + //Fields are deprecated and should not be exposed on storefront. + $customerData['group_id'] = null; + $customerData['id'] = null; $customerData['model'] = $customer; + + //'dob' is deprecated, 'date_of_birth' is used instead. + if (!empty($customerData['dob'])) { + $customerData['date_of_birth'] = $customerData['dob']; + } return $customerData; } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php index 6d33dea35835f..c690e11bd4940 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php @@ -70,7 +70,9 @@ public function resolve( if (!$this->newsLetterConfig->isActive(ScopeInterface::SCOPE_STORE)) { $args['input']['is_subscribed'] = false; } - + if (isset($args['input']['date_of_birth'])) { + $args['input']['dob'] = $args['input']['date_of_birth']; + } $customer = $this->createCustomerAccount->execute( $args['input'], $context->getExtensionAttributes()->getStore() diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php index b2ef03fc40e5a..f2b0d0e2a0495 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php @@ -70,6 +70,9 @@ public function resolve( if (empty($args['input']) || !is_array($args['input'])) { throw new GraphQlInputException(__('"input" value should be specified')); } + if (isset($args['input']['date_of_birth'])) { + $args['input']['dob'] = $args['input']['date_of_birth']; + } $customer = $this->getCustomer->execute($context); $this->updateCustomerAccount->execute( diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index d27debdc39c64..793ff0954ee94 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -36,13 +36,13 @@ input CustomerAddressInput { prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III") vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Address custom attributes") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.") } input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "Uniquely identifies the region") + region_id: Int @doc(description: "region_id is deprecated. Region ID is excessive on storefront and region code should suffice for all scenarios") } input CustomerAddressAttributeInput { @@ -60,10 +60,11 @@ input CustomerInput { middlename: String @doc(description: "The customer's middle name") lastname: String @doc(description: "The customer's family name") suffix: String @doc(description: "A value such as Sr., Jr., or III") - email: String @doc(description: "The customer's email address. Required") - dob: String @doc(description: "The customer's date of birth") + email: String! @doc(description: "The customer's email address. Required") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") password: String @doc(description: "The customer's password") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") } @@ -78,7 +79,7 @@ type RevokeCustomerTokenOutput { type Customer @doc(description: "Customer defines the customer name and address and other details") { created_at: String @doc(description: "Timestamp indicating when the account was created") - group_id: Int @doc(description: "The group assigned to the user. Default values are 0 (Not logged in), 1 (General), 2 (Wholesale), and 3 (Retailer)") + group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") firstname: String @doc(description: "The customer's first name") middlename: String @doc(description: "The customer's middle name") @@ -87,19 +88,20 @@ type Customer @doc(description: "Customer defines the customer name and address email: String @doc(description: "The customer's email address. Required") default_billing: String @doc(description: "The ID assigned to the billing address") default_shipping: String @doc(description: "The ID assigned to the shipping address") - dob: String @doc(description: "The customer's date of birth") - taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") - id: Int @doc(description: "The ID assigned to the customer") + dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") - gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") } type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ id: Int @doc(description: "The ID assigned to the address object") - customer_id: Int @doc(description: "The customer ID") + customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") - region_id: Int @doc(description: "A number that uniquely identifies the state, province, or other area") + region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios") country_id: String @doc(description: "The customer's country") street: [String] @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's company") @@ -112,17 +114,17 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III") - vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") - custom_attributes: [CustomerAddressAttribute] @doc(description: "Address custom attributes") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into container") extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes") } type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "Uniquely identifies the region") + region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios") } type CustomerAddressAttribute { diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 14759bd130f2b..f86ebaea69730 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CustomerImportExport\Model\Import; use Magento\Customer\Api\Data\CustomerInterface; @@ -21,7 +23,7 @@ class Customer extends AbstractCustomer { /** - * Attribute collection name + * Collection name attribute */ const ATTRIBUTE_COLLECTION_NAME = \Magento\Customer\Model\ResourceModel\Attribute\Collection::class; @@ -519,8 +521,10 @@ protected function _importData() ); } elseif ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE) { $processedData = $this->_prepareDataForUpdate($rowData); + // phpcs:disable Magento2.Performance.ForeachArrayMerge $entitiesToCreate = array_merge($entitiesToCreate, $processedData[self::ENTITIES_TO_CREATE_KEY]); $entitiesToUpdate = array_merge($entitiesToUpdate, $processedData[self::ENTITIES_TO_UPDATE_KEY]); + // phpcs:enable foreach ($processedData[self::ATTRIBUTES_TO_SAVE_KEY] as $tableName => $customerAttributes) { if (!isset($attributesToSave[$tableName])) { $attributesToSave[$tableName] = []; @@ -598,14 +602,18 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $isFieldNotSetAndCustomerDoesNotExist = !isset($rowData[$attributeCode]) && !$this->_getCustomerId($email, $website); $isFieldSetAndTrimmedValueIsEmpty - = isset($rowData[$attributeCode]) && '' === trim($rowData[$attributeCode]); + = isset($rowData[$attributeCode]) && '' === trim((string)$rowData[$attributeCode]); if ($isFieldRequired && ($isFieldNotSetAndCustomerDoesNotExist || $isFieldSetAndTrimmedValueIsEmpty)) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); continue; } - if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { + if (isset($rowData[$attributeCode]) && strlen((string)$rowData[$attributeCode])) { + if ($attributeParams['type'] == 'select') { + continue; + } + $this->isAttributeValid( $attributeCode, $attributeParams, diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 1c02d24f7e99c..06887fc0206fc 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -78,6 +78,11 @@ class DeployStaticOptions */ const NO_JAVASCRIPT = 'no-javascript'; + /** + * Key for js-bundle option + */ + const NO_JS_BUNDLE = 'no-js-bundle'; + /** * Key for css option */ @@ -122,9 +127,6 @@ class DeployStaticOptions */ const NO_LESS = 'no-less'; - /** - * Default jobs amount - */ const DEFAULT_JOBS_AMOUNT = 0; /** @@ -275,6 +277,12 @@ private function getSkipOptions() InputOption::VALUE_NONE, 'Do not deploy JavaScript files.' ), + new InputOption( + self::NO_JS_BUNDLE, + null, + InputOption::VALUE_NONE, + 'Do not deploy JavaScript bundle files.' + ), new InputOption( self::NO_CSS, null, diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 2e924d41a1b83..423f3072c4620 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -459,17 +459,17 @@ public function getParentMap() */ public function getParentFiles($type = null) { - $files = []; + $files = [[]]; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge - $files = array_merge($files, $parentPackage->getFiles()); + $files[] = $parentPackage->getFiles(); } else { // phpcs:ignore Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge - $files = array_merge($files, $parentPackage->getFilesByType($type)); + $files[] = $parentPackage->getFilesByType($type); } } - return $files; + return array_merge(...$files); } /** diff --git a/app/code/Magento/Deploy/Service/DeployPackage.php b/app/code/Magento/Deploy/Service/DeployPackage.php index 34a6b147a0551..90d4cdb116969 100644 --- a/app/code/Magento/Deploy/Service/DeployPackage.php +++ b/app/code/Magento/Deploy/Service/DeployPackage.php @@ -249,23 +249,6 @@ private function checkFileSkip($filePath, array $options) */ private function register(Package $package, PackageFile $file = null, $skipLogging = false) { - $logMessage = '.'; - if ($file) { - $logMessage = "Processing file '{$file->getSourcePath()}'"; - if ($file->getArea()) { - $logMessage .= " for area '{$file->getArea()}'"; - } - if ($file->getTheme()) { - $logMessage .= ", theme '{$file->getTheme()}'"; - } - if ($file->getLocale()) { - $logMessage .= ", locale '{$file->getLocale()}'"; - } - if ($file->getModule()) { - $logMessage .= "module '{$file->getModule()}'"; - } - } - $info = [ 'count' => $this->count, 'last' => $file ? $file->getSourcePath() : '' @@ -273,6 +256,23 @@ private function register(Package $package, PackageFile $file = null, $skipLoggi $this->deployStaticFile->writeTmpFile('info.json', $package->getPath(), json_encode($info)); if (!$skipLogging) { + $logMessage = '.'; + if ($file) { + $logMessage = "Processing file '{$file->getSourcePath()}'"; + if ($file->getArea()) { + $logMessage .= " for area '{$file->getArea()}'"; + } + if ($file->getTheme()) { + $logMessage .= ", theme '{$file->getTheme()}'"; + } + if ($file->getLocale()) { + $logMessage .= ", locale '{$file->getLocale()}'"; + } + if ($file->getModule()) { + $logMessage .= "module '{$file->getModule()}'"; + } + } + $this->logger->info($logMessage); } } diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 8903997159914..b6333d6fec71e 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -5,9 +5,9 @@ */ namespace Magento\Deploy\Service; -use Magento\Deploy\Strategy\DeployStrategyFactory; -use Magento\Deploy\Process\QueueFactory; use Magento\Deploy\Console\DeployStaticOptions as Options; +use Magento\Deploy\Process\QueueFactory; +use Magento\Deploy\Strategy\DeployStrategyFactory; use Magento\Framework\App\View\Deployment\Version\StorageInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; @@ -75,6 +75,9 @@ public function __construct( * @param array $options * @throws LocalizedException * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function deploy(array $options) { @@ -106,27 +109,35 @@ public function deploy(array $options) $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], - [ - 'queue' => $this->queueFactory->create($queueOptions) - ] + ['queue' => $this->queueFactory->create($queueOptions)] ); $packages = $deployStrategy->deploy($options); if ($options[Options::NO_JAVASCRIPT] !== true) { - $deployRjsConfig = $this->objectManager->create(DeployRequireJsConfig::class, [ - 'logger' => $this->logger - ]); - $deployI18n = $this->objectManager->create(DeployTranslationsDictionary::class, [ - 'logger' => $this->logger - ]); - $deployBundle = $this->objectManager->create(Bundle::class, [ - 'logger' => $this->logger - ]); + $deployRjsConfig = $this->objectManager->create( + DeployRequireJsConfig::class, + ['logger' => $this->logger] + ); + $deployI18n = $this->objectManager->create( + DeployTranslationsDictionary::class, + ['logger' => $this->logger] + ); foreach ($packages as $package) { if (!$package->isVirtual()) { $deployRjsConfig->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); $deployI18n->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); + } + } + } + + if ($options[Options::NO_JAVASCRIPT] !== true && $options[Options::NO_JS_BUNDLE] !== true) { + $deployBundle = $this->objectManager->create( + Bundle::class, + ['logger' => $this->logger] + ); + foreach ($packages as $package) { + if (!$package->isVirtual()) { $deployBundle->deploy($package->getArea(), $package->getTheme(), $package->getLocale()); } } diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml new file mode 100644 index 0000000000000..3a7d3663c8875 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MagentoDeveloperModeOnlyTestSuite"> + <before> + <magentoCLI command="deploy:mode:set developer" stepKey="enableDeveloperMode"/> + </before> + <include> + <group name="developer_mode_only"/> + </include> + <after> + <!-- Command should be uncommented once MQE-1711 is resolved --> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> + </after> + </suite> +</suites> diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml new file mode 100644 index 0000000000000..bf7014cdbb49d --- /dev/null +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MagentoProductionModeOnlyTestSuite"> + <before> + <!-- Command should be uncommented once MQE-1711 is resolved --> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> + </before> + <include> + <group name="production_mode_only"/> + </include> + <after> + <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> + </after> + </suite> +</suites> diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 396381960e544..fcc02476bb858 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -103,7 +103,7 @@ public function testDeploy($options, $expectedContentVersion) $package->expects($this->never())->method('getTheme'); $package->expects($this->never())->method('getLocale'); } else { - $package->expects($this->exactly(1))->method('isVirtual')->willReturn(false); + $package->expects($this->exactly(2))->method('isVirtual')->willReturn(false); $package->expects($this->exactly(3))->method('getArea')->willReturn('area'); $package->expects($this->exactly(3))->method('getTheme')->willReturn('theme'); $package->expects($this->exactly(3))->method('getLocale')->willReturn('locale'); @@ -198,6 +198,7 @@ public function deployDataProvider() [ 'strategy' => 'compact', 'no-javascript' => false, + 'no-js-bundle' => false, 'no-html-minify' => false, 'refresh-content-version-only' => false, ], @@ -207,6 +208,7 @@ public function deployDataProvider() [ 'strategy' => 'compact', 'no-javascript' => false, + 'no-js-bundle' => false, 'no-html-minify' => false, 'refresh-content-version-only' => false, 'content-version' => '123456', @@ -226,25 +228,28 @@ public function deployDataProvider() public function testMaxExecutionTimeOptionPassed() { $options = [ - DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::MAX_EXECUTION_TIME => 100, DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, - DeployStaticOptions::JOBS_AMOUNT => 3, - DeployStaticOptions::STRATEGY => 'compact', - DeployStaticOptions::NO_JAVASCRIPT => true, - DeployStaticOptions::NO_HTML_MINIFY => true, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_JS_BUNDLE => true, + DeployStaticOptions::NO_HTML_MINIFY => true, ]; $queueMock = $this->createMock(Queue::class); $strategyMock = $this->createMock(CompactDeploy::class); $this->queueFactory->expects($this->once()) ->method('create') - ->with([ - 'logger' => $this->logger, - 'maxExecTime' => 100, - 'maxProcesses' => 3, - 'options' => $options, - 'deployPackageService' => null - ]) + ->with( + [ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ] + ) ->willReturn($queueMock); $this->deployStrategyFactory->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml b/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml new file mode 100644 index 0000000000000..1256bb5443e04 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Mftf/Section/AdminShippingMethodDHLSection.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodDHLSection"> + <element name="carriersDHLTab" type="button" selector="#carriers_dhl-head"/> + <element name="carriersDHLActive" type="input" selector="#carriers_dhl_active_inherit"/> + <element name="carriersDHLTitle" type="input" selector="#carriers_dhl_title_inherit"/> + <element name="carriersDHLAccessId" type="input" selector="#carriers_dhl_id"/> + <element name="carriersDHLPassword" type="input" selector="#carriers_dhl_password"/> + <element name="carriersDHLAccount" type="input" selector="#carriers_dhl_account_inherit"/> + <element name="carriersDHLContentType" type="input" selector="#carriers_dhl_content_type_inherit"/> + <element name="carriersDHLHandlingType" type="input" selector="#carriers_dhl_handling_type_inherit"/> + <element name="carriersDHLHandlingAction" type="input" selector="#carriers_dhl_handling_action_inherit"/> + <element name="carriersDHLDivideOrderWeight" type="input" selector="#carriers_dhl_divide_order_weight_inherit"/> + <element name="carriersDHLUnitOfMeasure" type="input" selector="#carriers_dhl_unit_of_measure_inherit"/> + <element name="carriersDHLSize" type="input" selector="#carriers_dhl_size_inherit"/> + <element name="carriersDHLNonDocAllowedMethod" type="input" selector="#carriers_dhl_nondoc_methods_inherit"/> + <element name="carriersDHLSmartPostHubId" type="input" selector="#carriers_dhl_doc_methods_inherit"/> + <element name="carriersDHLSpecificErrMsg" type="input" selector="#carriers_dhl_specificerrmsg_inherit"/> + <element name="carriersDHLAllowSpecific" type="input" selector="#carriers_dhl_sallowspecific_inherit"/> + <element name="carriersDHLSpecificCountry" type="input" selector="#carriers_dhl_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..f5e1e8ef0c8ec --- /dev/null +++ b/app/code/Magento/Dhl/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in DHL section--> + <comment userInput="Assert configuration are disabled in DHL section" stepKey="commentSeeDisabledDHLConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodDHLSection.carriersDHLTab}}" dependentSelector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" visible="false" stepKey="expandDHLTab"/> + <waitForElementVisible selector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" stepKey="waitDHLTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLActive}}" userInput="disabled" stepKey="grabDHLActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLActiveDisabled" stepKey="assertDHLActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLTitle}}" userInput="disabled" stepKey="grabDHLTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLTitleDisabled" stepKey="assertDHLTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLAccessId}}" userInput="disabled" stepKey="grabDHLAccessIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLAccessIdDisabled" stepKey="assertDHLAccessIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLPassword}}" userInput="disabled" stepKey="grabDHLPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLPasswordDisabled" stepKey="assertDHLPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLAccount}}" userInput="disabled" stepKey="grabDHLAccountDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLAccountDisabled" stepKey="assertDHLAccountDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLContentType}}" userInput="disabled" stepKey="grabDHLContentTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLContentTypeDisabled" stepKey="assertDHLContentTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLHandlingType}}" userInput="disabled" stepKey="grabDHLHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLHandlingTypeDisabled" stepKey="assertDHLHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLHandlingAction}}" userInput="disabled" stepKey="grabDHLHandlingDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLHandlingDisabled" stepKey="assertDHLHandlingDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLDivideOrderWeight}}" userInput="disabled" stepKey="grabDHLDivideOrderWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLDivideOrderWeightDisabled" stepKey="assertDHLDivideOrderWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLUnitOfMeasure}}" userInput="disabled" stepKey="grabDHLUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLUnitOfMeasureDisabled" stepKey="assertDHLUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSize}}" userInput="disabled" stepKey="grabDHLSizeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSizeDisabled" stepKey="assertDHLSizeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLNonDocAllowedMethod}}" userInput="disabled" stepKey="grabDHLNonDocAllowedMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLNonDocAllowedMethodDisabled" stepKey="assertDHLNonDocAllowedMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSmartPostHubId}}" userInput="disabled" stepKey="grabDHLSmartPostHubIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSmartPostHubIdDisabled" stepKey="assertDHLSmartPostHubIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodDHLSection.carriersDHLSpecificErrMsg}}" userInput="disabled" stepKey="grabDHLSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabDHLSpecificErrMsgDisabled" stepKey="assertDHLSpecificErrMsgDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Directory/Model/Observer.php b/app/code/Magento/Directory/Model/Observer.php index e35c2de5cee5b..6d227e7148261 100644 --- a/app/code/Magento/Directory/Model/Observer.php +++ b/app/code/Magento/Directory/Model/Observer.php @@ -12,6 +12,11 @@ namespace Magento\Directory\Model; +/** + * Class Observer + * + * @package Magento\Directory\Model + */ class Observer { const CRON_STRING_PATH = 'crontab/default/jobs/currency_rates_update/schedule/cron_expr'; @@ -83,6 +88,8 @@ public function __construct( } /** + * Schedule update currency rates + * * @param mixed $schedule * @return void * @throws \Exception @@ -122,7 +129,7 @@ public function scheduledUpdateCurrencyRates($schedule) $importWarnings[] = __('FATAL ERROR:') . ' ' . __('Please specify the correct Import Service.'); } - if (sizeof($errors) > 0) { + if (count($errors) > 0) { foreach ($errors as $error) { $importWarnings[] = __('WARNING:') . ' ' . $error; } @@ -132,7 +139,7 @@ public function scheduledUpdateCurrencyRates($schedule) self::XML_PATH_ERROR_RECIPIENT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - if (sizeof($importWarnings) == 0) { + if (count($importWarnings) == 0) { $this->_currencyFactory->create()->saveRates($rates); } elseif ($errorRecipient) { //if $errorRecipient is not set, there is no sense send email to nobody diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php b/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php new file mode 100644 index 0000000000000..d7bec84e5440c --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddCountriesCaribbeanCuracaoKosovoSintMaarten.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Class AddCountriesCaribbeanCuracaoKosovoSintMaarten + * + * @package Magento\Directory\Setup\Patch + */ +class AddCountriesCaribbeanCuracaoKosovoSintMaarten implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * AddCountriesCaribbeanCuracaoKosovoSintMaarten constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** + * Fill table directory/country + */ + $data = [ + [ + 'country_id' => 'BQ', + 'iso2_code' => 'BQ', + 'iso3_code' => 'BES', + ], + [ + 'country_id' => 'CW', + 'iso2_code' => 'CW', + 'iso3_code' => 'CUW', + ], + [ + 'country_id' => 'SX', + 'iso2_code' => 'SX', + 'iso3_code' => 'SXM', + ], + [ + 'country_id' => 'XK', + 'iso2_code' => 'XK', + 'iso3_code' => 'XKX', + ], + ]; + + $this->moduleDataSetup->getConnection()->insertOnDuplicate( + $this->moduleDataSetup->getTable('directory_country'), + $data + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php new file mode 100644 index 0000000000000..7e53198cb9a4e --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Regions for Belgium. + */ +class AddDataForBelgium implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForBelgium() + ); + } + + /** + * Belgium states data. + * + * @return array + */ + private function getDataForBelgium() + { + return [ + ['BE', 'VAN', 'Antwerpen'], + ['BE', 'WBR', 'Brabant wallon'], + ['BE', 'BRU', 'Brussels Hoofdstedelijk Gewest'], + ['BE', 'WHT', 'Hainaut'], + ['BE', 'VLI', 'Limburg'], + ['BE', 'WLG', 'Liege'], + ['BE', 'WLX', 'Luxembourg'], + ['BE', 'WNA', 'Namur'], + ['BE', 'VOV', 'Oost-Vlaanderen'], + ['BE', 'VLG', 'Vlaams Gewest'], + ['BE', 'VBR', 'Vlaams-Brabant'], + ['BE', 'VWV', 'West-Vlaanderen'], + ['BE', 'WAL', 'Wallonne, Region'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php new file mode 100644 index 0000000000000..0750f8056c4d7 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add China States + */ +class AddDataForChina implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForChina() + ); + } + + /** + * China states data. + * + * @return array + */ + private function getDataForChina() + { + return [ + ['CN', 'CN-AH', 'Anhui Sheng'], + ['CN', 'CN-BJ', 'Beijing Shi'], + ['CN', 'CN-CQ', 'Chongqing Shi'], + ['CN', 'CN-FJ', 'Fujian Sheng'], + ['CN', 'CN-GS', 'Gansu Sheng'], + ['CN', 'CN-GD', 'Guangdong Sheng'], + ['CN', 'CN-GX', 'Guangxi Zhuangzu Zizhiqu'], + ['CN', 'CN-GZ', 'Guizhou Sheng'], + ['CN', 'CN-HI', 'Hainan Sheng'], + ['CN', 'CN-HE', 'Hebei Sheng'], + ['CN', 'CN-HL', 'Heilongjiang Sheng'], + ['CN', 'CN-HA', 'Henan Sheng'], + ['CN', 'CN-HK', 'Hong Kong SAR'], + ['CN', 'CN-HB', 'Hubei Sheng'], + ['CN', 'CN-HN', 'Hunan Sheng'], + ['CN', 'CN-JS', 'Jiangsu Sheng'], + ['CN', 'CN-JX', 'Jiangxi Sheng'], + ['CN', 'CN-JL', 'Jilin Sheng'], + ['CN', 'CN-LN', 'Liaoning Sheng'], + ['CN', 'CN-MO', 'Macao SAR'], + ['CN', 'CN-NM', 'Nei Mongol Zizhiqu'], + ['CN', 'CN-NX', 'Ningxia Huizi Zizhiqu'], + ['CN', 'CN-QH', 'Qinghai Sheng'], + ['CN', 'CN-SN', 'Shaanxi Sheng'], + ['CN', 'CN-SD', 'Shandong Sheng'], + ['CN', 'CN-SH', 'Shanghai Shi'], + ['CN', 'CN-SX', 'Shanxi Sheng'], + ['CN', 'CN-SC', 'Sichuan Sheng'], + ['CN', 'CN-TW', 'Taiwan Sheng'], + ['CN', 'CN-TJ', 'Tianjin Shi'], + ['CN', 'CN-XJ', 'Xinjiang Uygur Zizhiqu'], + ['CN', 'CN-XZ', 'Xizang Zizhiqu'], + ['CN', 'CN-YN', 'Yunnan Sheng'], + ['CN', 'CN-ZJ', 'Zhejiang Sheng'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/etc/adminhtml/system.xml b/app/code/Magento/Directory/etc/adminhtml/system.xml index 7d650b14b3d97..474b8357dfe1f 100644 --- a/app/code/Magento/Directory/etc/adminhtml/system.xml +++ b/app/code/Magento/Directory/etc/adminhtml/system.xml @@ -67,27 +67,45 @@ <field id="error_email" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Error Email Recipient</label> <validate>validate-email</validate> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="error_email_identity" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Error Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="error_email_template" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Error Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="frequency" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Frequency</label> <source_model>Magento\Cron\Model\Config\Source\Frequency</source_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="service" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Service</label> <source_model>Magento\Directory\Model\Currency\Import\Source\Service</source_model> <backend_model>Magento\Config\Model\Config\Backend\Currency\Cron</backend_model> + <depends> + <field id="enabled">1</field> + </depends> </field> <field id="time" translate="label" type="time" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Start Time</label> + <depends> + <field id="enabled">1</field> + </depends> </field> </group> </section> diff --git a/app/code/Magento/Directory/etc/config.xml b/app/code/Magento/Directory/etc/config.xml index c18c4f29d5822..2ff0b484fe979 100644 --- a/app/code/Magento/Directory/etc/config.xml +++ b/app/code/Magento/Directory/etc/config.xml @@ -36,7 +36,7 @@ <general> <country> <optional_zip_countries>HK,IE,MO,PA,GB</optional_zip_countries> - <allow>AF,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AX,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BL,BT,BO,BA,BW,BV,BR,IO,VG,BN,BG,BF,BI,KH,CM,CA,CD,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CK,CR,HR,CU,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GG,GH,GI,GR,GL,GD,GP,GU,GT,GN,GW,GY,HT,HM,HN,HK,HU,IS,IM,IN,ID,IR,IQ,IE,IL,IT,CI,JE,JM,JP,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,ME,MF,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,FX,MX,FM,MD,MC,MN,MS,MA,MZ,MM,NA,NR,NP,NL,AN,NC,NZ,NI,NE,NG,NU,NF,KP,MP,NO,OM,PK,PW,PA,PG,PY,PE,PH,PN,PL,PS,PT,PR,QA,RE,RO,RS,RU,RW,SH,KN,LC,PM,VC,WS,SM,ST,SA,SN,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TL,TW,TJ,TZ,TH,TG,TK,TO,TT,TN,TR,TM,TC,TV,VI,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,WF,EH,YE,ZM,ZW</allow> + <allow>AF,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AX,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BL,BT,BO,BQ,BA,BW,BV,BR,IO,VG,BN,BG,BF,BI,KH,CM,CA,CD,CV,KY,CF,TD,CL,CN,CX,CW,CC,CO,KM,CG,CK,CR,HR,CU,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GG,GH,GI,GR,GL,GD,GP,GU,GT,GN,GW,GY,HT,HM,HN,HK,HU,IS,IM,IN,ID,IR,IQ,IE,IL,IT,CI,JE,JM,JP,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,ME,MF,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,FX,MX,FM,MD,MC,MN,MS,MA,MZ,MM,NA,NR,NP,NL,AN,NC,NZ,NI,NE,NG,NU,NF,KP,MP,NO,OM,PK,PW,PA,PG,PY,PE,PH,PN,PL,PS,PT,PR,QA,RE,RO,RS,RU,RW,SH,KN,LC,PM,VC,WS,SM,ST,SA,SN,SC,SL,SG,SK,SI,SB,SO,ZA,GS,KR,ES,LK,SD,SR,SJ,SZ,SE,CH,SX,SY,TL,TW,TJ,TZ,TH,TG,TK,TO,TT,TN,TR,TM,TC,TV,VI,UG,UA,AE,GB,US,UM,UY,UZ,VU,VA,VE,VN,WF,EH,XK,YE,ZM,ZW</allow> <default>US</default> </country> <locale> diff --git a/app/code/Magento/Directory/etc/crontab.xml b/app/code/Magento/Directory/etc/crontab.xml index d6868ff6aa0d6..589cd394d7cf1 100644 --- a/app/code/Magento/Directory/etc/crontab.xml +++ b/app/code/Magento/Directory/etc/crontab.xml @@ -7,6 +7,8 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> <group id="default"> - <job name="currency_rates_update" instance="Magento\Directory\Model\Observer" method="scheduledUpdateCurrencyRates" /> + <job name="currency_rates_update" instance="Magento\Directory\Model\Observer" method="scheduledUpdateCurrencyRates"> + <config_path>crontab/default/jobs/currency_rates_update/schedule/cron_expr</config_path> + </job> </group> </config> diff --git a/app/code/Magento/Directory/etc/db_schema.xml b/app/code/Magento/Directory/etc/db_schema.xml index c11e0ee525e37..163e972423b98 100644 --- a/app/code/Magento/Directory/etc/db_schema.xml +++ b/app/code/Magento/Directory/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="directory_country" resource="default" engine="innodb" comment="Directory Country"> - <column xsi:type="varchar" name="country_id" nullable="false" length="2" comment="Country Id in ISO-2"/> + <column xsi:type="varchar" name="country_id" nullable="false" length="2" comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="iso2_code" nullable="true" length="2" comment="Country ISO-2 format"/> <column xsi:type="varchar" name="iso3_code" nullable="true" length="3" comment="Country ISO-3"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -17,8 +17,8 @@ </table> <table name="directory_country_format" resource="default" engine="innodb" comment="Directory Country Format"> <column xsi:type="int" name="country_format_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Country Format Id"/> - <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country Id in ISO-2"/> + comment="Country Format ID"/> + <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="type" nullable="true" length="30" comment="Country Format Type"/> <column xsi:type="text" name="format" nullable="false" comment="Country Format"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -31,9 +31,9 @@ </table> <table name="directory_country_region" resource="default" engine="innodb" comment="Directory Country Region"> <column xsi:type="int" name="region_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Region Id"/> + comment="Region ID"/> <column xsi:type="varchar" name="country_id" nullable="false" length="4" default="0" - comment="Country Id in ISO-2"/> + comment="Country ID in ISO-2"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Region code"/> <column xsi:type="varchar" name="default_name" nullable="true" length="255" comment="Region Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -47,7 +47,7 @@ comment="Directory Country Region Name"> <column xsi:type="varchar" name="locale" nullable="false" length="8" comment="Locale"/> <column xsi:type="int" name="region_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Region Id"/> + default="0" comment="Region ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Region Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="locale"/> diff --git a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php deleted file mode 100644 index caf2f7745a3dd..0000000000000 --- a/app/code/Magento/Downloadable/Setup/Patch/Schema/ChangeTmpTablesEngine.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Setup\Patch\Schema; - -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Change engine for temporary tables to InnoDB. - */ -class ChangeTmpTablesEngine implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct(SchemaSetupInterface $schemaSetup) - { - $this->schemaSetup = $schemaSetup; - } - - /** - * @inheritdoc - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $tableName = $this->schemaSetup->getTable('catalog_product_index_price_downlod_tmp'); - if ($this->schemaSetup->getConnection()->isTableExists($tableName)) { - $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB'); - } - - $this->schemaSetup->endSetup(); - } - - /** - * @inheritdoc - */ - public static function getDependencies() - { - return []; - } - - /** - * @inheritdoc - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 1a6be43b38d2c..2986532ef1138 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -74,6 +74,21 @@ <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> <requiredEntity type="downloadable_link">apiDownloadableLink</requiredEntity> </entity> + <entity name="ApiDownloadableProductUnderscoredSku" 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> <entity name="DownloadableProductWithTwoLink100" type="product"> <data key="sku" unique="suffix">downloadableproduct</data> <data key="type_id">downloadable</data> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml index 543aea7d8297f..dc2a58be138e7 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml @@ -14,5 +14,7 @@ <element name="downloadableLinkByTitle" type="input" selector="//*[@id='downloadable-links-list']/*[contains(.,'{{title}}')]//input" parameterized="true" timeout="30"/> <element name="downloadableLinkSampleByTitle" type="text" selector="//label[contains(., '{{title}}')]/a[contains(@class, 'sample link')]" parameterized="true"/> <element name="downloadableSampleLabel" type="text" selector="//a[contains(.,normalize-space('{{title}}'))]" parameterized="true" timeout="30"/> + <element name="downloadableLinkSelectAllCheckbox" type="checkbox" selector="#links_all" /> + <element name="downloadableLinkSelectAllLabel" type="text" selector="label[for='links_all']" /> </section> </sections> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml new file mode 100644 index 0000000000000..8cb0d5fde9863 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDownloadableProductTypeSwitchingToConfigurableProductTest" extends="AdminSimpleProductTypeSwitchingToConfigurableProductTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Product type switching"/> + <title value="Downloadable product type switching on editing to configurable product"/> + <description value="Downloadable product type switching on editing to configurable product"/> + <testCaseId value="MC-17957"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <!-- Open Dropdown and select downloadable product option --> + <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection" after="waitForSimpleProductPageLoad"/> + <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkOptionIsDownloadable" after="openDownloadableSection"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeightForProduct" after="checkOptionIsDownloadable"/> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm" after="selectWeightForProduct"/> + </test> + <test name="AdminSimpleProductTypeSwitchingToDownloadableProductTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Product type switching"/> + <title value="Simple product type switching on editing to downloadable product"/> + <description value="Simple product type switching on editing to downloadable product"/> + <testCaseId value="MC-17956"/> + <useCaseId value="MAGETWO-44170"/> + <severity value="MAJOR"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create product--> + <comment userInput="Create product" stepKey="commentCreateProduct"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete product--> + <comment userInput="Delete product" stepKey="commentDeleteProduct"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Change product type to Downloadable--> + <comment userInput="Change product type to Downloadable" stepKey="commentCreateDownloadable"/> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="gotToDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForDownloadableProductPageLoad"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has no weight" stepKey="selectNoWeightForProduct"/> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOptionPurchaseSeparately"/> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveDownloadableProductForm"/> + <!--Assert downloadable product on Admin product page grid--> + <comment userInput="Assert configurable product in Admin product page grid" stepKey="commentAssertDownloadableProductOnAdmin"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="goToCatalogProductPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> + <!--Assert downloadable product on storefront--> + <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="openDownloadableProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontDownloadableProductPageLoad"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertDownloadableProductInStock"/> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinksInStorefront"/> + <seeElement selector="{{StorefrontDownloadableProductSection.downloadableLinkLabel(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeDownloadableLink" /> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml index 66177b6875dd9..39260b897ee19 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml @@ -39,7 +39,7 @@ <group value="Downloadable"/> </annotations> <before> - <createData entity="ApiDownloadableProduct" stepKey="product"/> + <createData entity="ApiDownloadableProductUnderscoredSku" stepKey="product"/> <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="product"/> </createData> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml new file mode 100644 index 0000000000000..0b905964fd2d9 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="EditDownloadableProductWithSeparateLinksFromCartTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Edit downloadable product with separate links from cart test"/> + <description value="Product price should remain correct when editing downloadable product with separate links from cart."/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: Add checkbox for first link --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" + stepKey="selectProductLink"/> + + <!-- Step 3: Add the Product to cart --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="DownloadableProduct"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Step 4: Open cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="openShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$51.99" + stepKey="assertProductPriceInCart"/> + + <!-- Step 5: Edit Product in cart --> + <click selector="{{CheckoutCartProductSection.nthEditButton('1')}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForEditPage"/> + + <!-- Step 6: Make sure Product price is correct --> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="51.99" stepKey="checkPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml new file mode 100644 index 0000000000000..b0424b1976c1c --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ManualSelectAllDownloadableLinksDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Manual select all downloadable links downloadable product test"/> + <description value="Manually selecting all downloadable links must change 'Select/Unselect all' button label to 'Unselect all', and 'Select all' otherwise"/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: Check first downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" + stepKey="selectFirstCheckbox"/> + + <!-- Step 3: Check second downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" + stepKey="selectSecondCheckbox"/> + + <!-- Step 4: Grab "Select/Unselect All" button label text --> + <grabTextFrom + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllLabel}}" + stepKey="grabUnselectAllButtonText"/> + + <!-- Step 5: Assert that 'Select/Unselect all' button text is 'Unselect all' after manually checking all checkboxes --> + <assertEquals + message="Assert that 'Select/Unselect all' button text is 'Unselect all' after manually checking all checkboxes" + stepKey="assertButtonTextOne"> + <expectedResult type="string">Unselect all</expectedResult> + <actualResult type="string">{$grabUnselectAllButtonText}</actualResult> + </assertEquals> + + <!-- Step 6: Uncheck second downloadable link checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" + stepKey="unselectSecondCheckbox"/> + + <!-- Step 7: Grab "Select/Unselect All" button label text --> + <grabTextFrom + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllLabel}}" + stepKey="grabSelectAllButtonText"/> + + <!-- Step 8: Assert that 'Select/Unselect all' button text is 'Select all' after manually unchecking one checkbox --> + <assertEquals + message="Assert that 'Select/Unselect all' button text is 'Select all' after manually unchecking one checkbox" + stepKey="assertButtonTextTwo"> + <expectedResult type="string">Select all</expectedResult> + <actualResult type="string">{$grabSelectAllButtonText}</actualResult> + </assertEquals> + + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml new file mode 100644 index 0000000000000..94940f0e08195 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SelectAllDownloadableLinksDownloadableProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Downloadable Product"/> + <title value="Select all downloadable links downloadable product test"/> + <description value="All the downloadable links must be selected or unselected when anyone click on select all or unselect all checkbox respectively."/> + <severity value="MAJOR"/> + <group value="Downloadable"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + + <!-- Create downloadable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Fill downloadable link information before the creation link --> + <actionGroup ref="AdminAddDownloadableLinkInformationActionGroup" stepKey="addDownloadableLinkInformation"/> + + <!-- Links can be purchased separately --> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" + stepKey="checkOptionPurchaseSeparately"/> + + <!-- Add first downloadable link --> + <actionGroup ref="addDownloadableProductLinkWithMaxDownloads" stepKey="addFirstDownloadableProductLink"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + + <!-- Add second downloadable link --> + <actionGroup ref="addDownloadableProductLink" stepKey="addSecondDownloadableProductLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Step 1: Navigate to store front Product page as guest --> + <amOnPage url="/{{DownloadableProduct.sku}}.html" + stepKey="amOnStorefrontProductPage"/> + + <!-- Step 2: click on select all checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllCheckbox}}" + stepKey="selectAllProductLink"/> + + <!-- Step 3: Make sure that all product links are checked --> + <seeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeFirstCheckboxChecked"/> + + <seeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="seeSecondCheckboxChecked"/> + + <!-- Step 4: click again on select all checkbox --> + <click + selector="{{StorefrontDownloadableProductSection.downloadableLinkSelectAllCheckbox}}" + stepKey="unselectAllProductLink"/> + + <!-- Step 5: Make sure that all product links are unchecked --> + <dontSeeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="seeFirstCheckboxUnChecked"/> + + <dontSeeCheckboxIsChecked selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="seeSecondCheckboxUnChecked"/> + + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml new file mode 100644 index 0000000000000..7174122760576 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchDownloadableBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Downloadable product with product sku that contains hyphen"/> + <description value="Guest customer should be able to advance search Downloadable product with product that contains hyphen"/> + <severity value="MAJOR"/> + <testCaseId value="MC-252"/> + <group value="Downloadable"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiDownloadableProduct" stepKey="product"/> + <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="product"/> + </createData> + <createData entity="ApiDownloadableLink" stepKey="addDownloadableLink2"> + <requiredEntity createDataKey="product"/> + </createData> + </before> + </test> + </tests> diff --git a/app/code/Magento/Downloadable/etc/db_schema.xml b/app/code/Magento/Downloadable/etc/db_schema.xml index ccbefa4fb3992..ee7b3c5683ea1 100644 --- a/app/code/Magento/Downloadable/etc/db_schema.xml +++ b/app/code/Magento/Downloadable/etc/db_schema.xml @@ -233,7 +233,7 @@ <column name="website_id"/> </constraint> </table> - <table name="catalog_product_index_price_downlod_tmp" resource="default" engine="memory" + <table name="catalog_product_index_price_downlod_tmp" resource="default" engine="innodb" comment="Temporary Indexer Table for price of downloadable products"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> diff --git a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js index 09a5ad1afa9ec..8bdea0b3a70b6 100644 --- a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js +++ b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js @@ -12,12 +12,17 @@ define([ ], function ($) { 'use strict'; + /** + * Downloadable widget + */ $.widget('mage.downloadable', { options: { priceHolderSelector: '.price-box' }, - /** @inheritdoc */ + /** + * @inheritdoc + */ _create: function () { var self = this; @@ -38,6 +43,8 @@ define([ }); } }); + + this._reloadPrice(); }, /** @@ -63,6 +70,32 @@ define([ } } }); + + this.reloadAllCheckText(); + }, + + /** + * Reload all-elements-checkbox's label + * @private + */ + reloadAllCheckText: function () { + var allChecked = true, + allElementsCheck = $(this.options.allElements), + allElementsLabel = $('label[for="' + allElementsCheck.attr('id') + '"] > span'); + + $(this.options.linkElement).each(function () { + if (!this.checked) { + allChecked = false; + } + }); + + if (allChecked) { + allElementsLabel.text(allElementsCheck.attr('data-checked')); + allElementsCheck.prop('checked', true); + } else { + allElementsLabel.text(allElementsCheck.attr('data-notchecked')); + allElementsCheck.prop('checked', false); + } } }); diff --git a/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php b/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php new file mode 100644 index 0000000000000..100d5638a96da --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeGroupSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Attribute Group search results. + */ +class AttributeGroupSearchResults extends SearchResults implements AttributeGroupSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/AttributeSearchResults.php b/app/code/Magento/Eav/Model/AttributeSearchResults.php new file mode 100644 index 0000000000000..b82a27bbfea1a --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Eav Attribute search results. + */ +class AttributeSearchResults extends SearchResults implements AttributeSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/AttributeSetSearchResults.php b/app/code/Magento/Eav/Model/AttributeSetSearchResults.php new file mode 100644 index 0000000000000..46592efda5a17 --- /dev/null +++ b/app/code/Magento/Eav/Model/AttributeSetSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model; + +use Magento\Eav\Api\Data\AttributeSetSearchResultsInterface; +use Magento\Framework\Api\SearchResults; + +/** + * Service Data Object with Attribute Set search results. + */ +class AttributeSetSearchResults extends SearchResults implements AttributeSetSearchResultsInterface +{ +} diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index bb2477d4df827..8bd9ca2cc03c8 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -32,12 +32,12 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im const ATTRIBUTE_CODE_MAX_LENGTH = 60; /** - * Attribute code min length. + * Min accepted length of an attribute code. */ const ATTRIBUTE_CODE_MIN_LENGTH = 1; /** - * Cache tag + * Tag to use for attributes caching. */ const CACHE_TAG = 'EAV_ATTRIBUTE'; @@ -311,7 +311,7 @@ public function beforeSave() } /** - * Save additional data + * @inheritdoc * * @return $this * @throws LocalizedException @@ -489,6 +489,9 @@ public function getIdentities() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -502,6 +505,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 3857118ae67ca..16fe495de18db 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -1405,6 +1405,9 @@ public function setExtensionAttributes(\Magento\Eav\Api\Data\AttributeExtensionI /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -1430,6 +1433,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index d05a7e1e2baa4..c5077733e10ae 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -761,6 +761,9 @@ public function getValidAttributeIds($attributeIds) * * @return array * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -774,6 +777,9 @@ public function __sleep() * * @return void * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index cd0d5141154c0..15dcea077c887 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +namespace Magento\Eav\Model\Validator\Attribute; + +use Magento\Eav\Model\Attribute; + /** * EAV attribute data validator * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Eav\Model\Validator\Attribute; - -use Magento\Eav\Model\Attribute; - class Data extends \Magento\Framework\Validator\AbstractValidator { /** @@ -126,7 +126,7 @@ public function isValid($entity) $dataModel = $this->_attrDataFactory->create($attribute, $entity); $dataModel->setExtractedData($data); if (!isset($data[$attributeCode])) { - $data[$attributeCode] = null; + $data[$attributeCode] = ''; } $result = $dataModel->validateValue($data[$attributeCode]); if (true !== $result) { diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 07ce6fbfc6a4c..acba37cc45788 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -4,13 +4,54 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; + /** * Test for \Magento\Eav\Model\Validator\Attribute\Data */ -namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; - class DataTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Eav\Model\AttributeDataFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $attrDataFactory; + + /** + * @var \Magento\Eav\Model\Validator\Attribute\Data + */ + private $model; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->attrDataFactory = $this->getMockBuilder(\Magento\Eav\Model\AttributeDataFactory::class) + ->setMethods(['create']) + ->setConstructorArgs( + [ + 'objectManager' => $this->createMock(\Magento\Framework\ObjectManagerInterface::class), + 'string' => $this->createMock(\Magento\Framework\Stdlib\StringUtils::class) + ] + ) + ->getMock(); + + $this->model = $this->objectManager->getObject( + \Magento\Eav\Model\Validator\Attribute\Data::class, + [ + '_attrDataFactory' => $this->attrDataFactory + ] + ); + } + /** * Testing \Magento\Eav\Model\Validator\Attribute\Data::isValid * @@ -381,13 +422,15 @@ public function testAddErrorMessages() protected function _getAttributeMock($attributeData) { $attribute = $this->getMockBuilder(\Magento\Eav\Model\Attribute::class) - ->setMethods([ - 'getAttributeCode', - 'getDataModel', - 'getFrontendInput', - '__wakeup', - 'getIsVisible', - ]) + ->setMethods( + [ + 'getAttributeCode', + 'getDataModel', + 'getFrontendInput', + '__wakeup', + 'getIsVisible', + ] + ) ->disableOriginalConstructor() ->getMock(); @@ -436,7 +479,7 @@ protected function _getDataModelMock($returnValue, $argument = null) $dataModel = $this->getMockBuilder( \Magento\Eav\Model\Attribute\Data\AbstractData::class )->disableOriginalConstructor()->setMethods( - ['validateValue'] + ['setExtractedData', 'validateValue'] )->getMockForAbstractClass(); if ($argument) { $dataModel->expects( @@ -466,4 +509,24 @@ protected function _getEntityMock() )->disableOriginalConstructor()->getMock(); return $entity; } + + /** + * Test for isValid() without data for attribute. + * + * @return void + */ + public function testIsValidWithoutData() : void + { + $attributeData = ['attribute_code' => 'attribute', 'frontend_input' => 'text', 'is_visible' => true]; + $entity = $this->_getEntityMock(); + $attribute = $this->_getAttributeMock($attributeData); + $dataModel = $this->_getDataModelMock(true, $this->logicalAnd($this->isEmpty(), $this->isType('string'))); + $dataModel->expects($this->once())->method('setExtractedData')->with([])->willReturnSelf(); + $this->attrDataFactory->expects($this->once()) + ->method('create') + ->with($attribute, $entity) + ->willReturn($dataModel); + $this->model->setAttributes([$attribute])->setData([]); + $this->assertTrue($this->model->isValid($entity)); + } } diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml index b6c42d725e5e9..407aa8976d684 100644 --- a/app/code/Magento/Eav/etc/db_schema.xml +++ b/app/code/Magento/Eav/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="eav_entity_type" resource="default" engine="innodb" comment="Eav Entity Type"> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Entity Type Id"/> + comment="Entity Type ID"/> <column xsi:type="varchar" name="entity_type_code" nullable="false" length="50" comment="Entity Type Code"/> <column xsi:type="varchar" name="entity_model" nullable="false" length="255" comment="Entity Model"/> <column xsi:type="varchar" name="attribute_model" nullable="true" length="255" comment="Attribute Model"/> @@ -21,7 +21,7 @@ <column xsi:type="varchar" name="data_sharing_key" nullable="true" length="100" default="default" comment="Data Sharing Key"/> <column xsi:type="smallint" name="default_attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Attribute Set Id"/> + identity="false" default="0" comment="Default Attribute Set ID"/> <column xsi:type="varchar" name="increment_model" nullable="true" length="255" comment="Increment Model"/> <column xsi:type="smallint" name="increment_per_store" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Increment Per Store"/> @@ -44,14 +44,14 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + identity="false" default="0" comment="Attribute Set ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Parent Id"/> + default="0" comment="Parent ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -75,13 +75,13 @@ </table> <table name="eav_entity_datetime" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Attribute Value"/> @@ -115,13 +115,13 @@ </table> <table name="eav_entity_decimal" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" @@ -156,13 +156,13 @@ </table> <table name="eav_entity_int" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" @@ -196,13 +196,13 @@ </table> <table name="eav_entity_text" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="false" comment="Attribute Value"/> @@ -233,13 +233,13 @@ </table> <table name="eav_entity_varchar" resource="default" engine="innodb" comment="Eav Entity Value Prefix"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Attribute Value"/> @@ -273,9 +273,9 @@ </table> <table name="eav_attribute" resource="default" engine="innodb" comment="Eav Attribute"> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="varchar" name="attribute_code" nullable="false" length="255" comment="Attribute Code"/> <column xsi:type="varchar" name="attribute_model" nullable="true" length="255" comment="Attribute Model"/> <column xsi:type="varchar" name="backend_model" nullable="true" length="255" comment="Backend Model"/> @@ -308,13 +308,13 @@ </table> <table name="eav_entity_store" resource="default" engine="innodb" comment="Eav Entity Store"> <column xsi:type="int" name="entity_store_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Store Id"/> + comment="Entity Store ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="increment_prefix" nullable="true" length="20" comment="Increment Prefix"/> - <column xsi:type="varchar" name="increment_last_id" nullable="true" length="50" comment="Last Incremented Id"/> + <column xsi:type="varchar" name="increment_last_id" nullable="true" length="50" comment="Last Incremented ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_store_id"/> </constraint> @@ -332,9 +332,9 @@ </table> <table name="eav_attribute_set" resource="default" engine="innodb" comment="Eav Attribute Set"> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Attribute Set Id"/> + comment="Attribute Set ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="varchar" name="attribute_set_name" nullable="true" length="255" comment="Attribute Set Name"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> @@ -355,15 +355,15 @@ </table> <table name="eav_attribute_group" resource="default" engine="innodb" comment="Eav Attribute Group"> <column xsi:type="smallint" name="attribute_group_id" padding="5" unsigned="true" nullable="false" - identity="true" comment="Attribute Group Id"/> + identity="true" comment="Attribute Group ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> + identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="varchar" name="attribute_group_name" nullable="true" length="255" comment="Attribute Group Name"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="default_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Default Id"/> + default="0" comment="Default ID"/> <column xsi:type="varchar" name="attribute_group_code" nullable="false" length="255" comment="Attribute Group Code"/> <column xsi:type="varchar" name="tab_group_code" nullable="true" length="255" comment="Tab Group Code"/> @@ -388,15 +388,15 @@ </table> <table name="eav_entity_attribute" resource="default" engine="innodb" comment="Eav Entity Attributes"> <column xsi:type="int" name="entity_attribute_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Attribute Id"/> + comment="Entity Attribute ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Type Id"/> + default="0" comment="Entity Type ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Set Id"/> + identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="smallint" name="attribute_group_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Attribute Group Id"/> + identity="false" default="0" comment="Attribute Group ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -426,9 +426,9 @@ </table> <table name="eav_attribute_option" resource="default" engine="innodb" comment="Eav Attribute Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -443,11 +443,11 @@ </table> <table name="eav_attribute_option_value" resource="default" engine="innodb" comment="Eav Attribute Option Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Option Id"/> + default="0" comment="Option ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> @@ -458,6 +458,10 @@ <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID" table="eav_attribute_option_value" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_OPTION_ID"> + <column name="store_id"/> + <column name="option_id"/> + </constraint> <index referenceId="EAV_ATTRIBUTE_OPTION_VALUE_OPTION_ID" indexType="btree"> <column name="option_id"/> </index> @@ -467,11 +471,11 @@ </table> <table name="eav_attribute_label" resource="default" engine="innodb" comment="Eav Attribute Label"> <column xsi:type="int" name="attribute_label_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Attribute Label Id"/> + comment="Attribute Label ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_label_id"/> @@ -481,6 +485,10 @@ referenceColumn="attribute_id" onDelete="CASCADE"/> <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" table="eav_attribute_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_STORE_ID_UNIQUE"> + <column name="store_id"/> + <column name="attribute_id"/> + </constraint> <index referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> @@ -491,14 +499,14 @@ </table> <table name="eav_form_type" resource="default" engine="innodb" comment="Eav Form Type"> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/> <column xsi:type="varchar" name="label" nullable="false" length="255" comment="Label"/> <column xsi:type="smallint" name="is_system" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is System"/> <column xsi:type="varchar" name="theme" nullable="true" length="64" comment="Theme"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> </constraint> @@ -515,9 +523,9 @@ </table> <table name="eav_form_type_entity" resource="default" engine="innodb" comment="Eav Form Type Entity"> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Entity Type Id"/> + comment="Entity Type ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> <column name="entity_type_id"/> @@ -534,9 +542,9 @@ </table> <table name="eav_form_fieldset" resource="default" engine="innodb" comment="Eav Form Fieldset"> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> @@ -552,9 +560,9 @@ </table> <table name="eav_form_fieldset_label" resource="default" engine="innodb" comment="Eav Form Fieldset Label"> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="false" length="255" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="fieldset_id"/> @@ -572,13 +580,13 @@ </table> <table name="eav_form_element" resource="default" engine="innodb" comment="Eav Form Element"> <column xsi:type="int" name="element_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Element Id"/> + comment="Element ID"/> <column xsi:type="smallint" name="type_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Type Id"/> + comment="Type ID"/> <column xsi:type="smallint" name="fieldset_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Fieldset Id"/> + comment="Fieldset ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Eav/etc/db_schema_whitelist.json b/app/code/Magento/Eav/etc/db_schema_whitelist.json index b3f1aca50df01..1814c7ba2dbc3 100644 --- a/app/code/Magento/Eav/etc/db_schema_whitelist.json +++ b/app/code/Magento/Eav/etc/db_schema_whitelist.json @@ -287,7 +287,8 @@ "constraint": { "PRIMARY": true, "EAV_ATTR_OPT_VAL_OPT_ID_EAV_ATTR_OPT_OPT_ID": true, - "EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID": true + "EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID": true, + "EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_OPTION_ID": true } }, "eav_attribute_label": { @@ -304,7 +305,8 @@ "constraint": { "PRIMARY": true, "EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true, - "EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID": true + "EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID": true, + "EAV_ATTRIBUTE_LABEL_STORE_ID_ATTRIBUTE_ID": true } }, "eav_form_type": { diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index db6f9b0a64f9f..a09dc28399858 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -22,9 +22,9 @@ <preference for="Magento\Eav\Api\AttributeOptionManagementInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> <preference for="Magento\Eav\Api\Data\AttributeOptionLabelInterface" type="Magento\Eav\Model\Entity\Attribute\OptionLabel" /> <preference for="Magento\Eav\Api\Data\AttributeValidationRuleInterface" type="Magento\Eav\Model\Entity\Attribute\ValidationRule" /> - <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Eav\Api\Data\AttributeSetSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Eav\Model\AttributeSearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeSetSearchResultsInterface" type="Magento\Eav\Model\AttributeSetSearchResults" /> + <preference for="Magento\Eav\Api\Data\AttributeGroupSearchResultsInterface" type="Magento\Eav\Model\AttributeGroupSearchResults" /> <preference for="Magento\Framework\Webapi\CustomAttributeTypeLocatorInterface" type="Magento\Eav\Model\TypeLocator" /> <type name="Magento\Eav\Model\Entity\Attribute\Config"> diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 97a76de4b995a..f6d0a6318a5f2 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -195,15 +195,11 @@ public function cleanIndex($storeId, $mappedIndexerId) { $this->checkIndex($storeId, $mappedIndexerId, true); $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex); - if ($this->client->isEmptyIndex($indexName)) { - // use existing index if empty - return $this; - } // prepare new index name and increase version $indexPattern = $this->indexNameResolver->getIndexPattern($storeId, $mappedIndexerId); $version = (int)(str_replace($indexPattern, '', $indexName)); - $newIndexName = $indexPattern . ++$version; + $newIndexName = $indexPattern . (++$version); // remove index if already exists if ($this->client->indexExists($newIndexName)) { @@ -354,12 +350,14 @@ protected function prepareIndex($storeId, $indexName, $mappedIndexerId) { $this->indexBuilder->setStoreId($storeId); $settings = $this->indexBuilder->build(); - $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes([ - 'entityType' => $mappedIndexerId, - // Use store id instead of website id from context for save existing fields mapping. - // In future websiteId will be eliminated due to index stored per store - 'websiteId' => $storeId - ]); + $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes( + [ + 'entityType' => $mappedIndexerId, + // Use store id instead of website id from context for save existing fields mapping. + // In future websiteId will be eliminated due to index stored per store + 'websiteId' => $storeId + ] + ); $settings['index']['mapping']['total_fields']['limit'] = $this->getMappingTotalFieldsLimit($allAttributeTypes); $this->client->createIndex($indexName, ['settings' => $settings]); $this->client->addFieldsMapping( diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php index afd383c13421f..ddf75c0a78e25 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -138,7 +138,12 @@ protected function buildQueries(array $matches, array $queryValue) $transformedTypes = []; foreach ($matches as $match) { - $attributeAdapter = $this->attributeProvider->getByAttributeCode($match['field']); + $resolvedField = $this->fieldMapper->getFieldName( + $match['field'], + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + + $attributeAdapter = $this->attributeProvider->getByAttributeCode($resolvedField); $fieldType = $this->fieldTypeResolver->getFieldType($attributeAdapter); $valueTransformer = $this->valueTransformerPool->get($fieldType ?? 'text'); $valueTransformerHash = \spl_object_hash($valueTransformer); @@ -151,10 +156,6 @@ protected function buildQueries(array $matches, array $queryValue) continue; } - $resolvedField = $this->fieldMapper->getFieldName( - $match['field'], - ['type' => FieldMapperInterface::TYPE_QUERY] - ); $conditions[] = [ 'condition' => $queryValue['condition'], 'body' => [ diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php index ec50ba7b3d5df..326c04aad6165 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -77,6 +77,7 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase * Setup * * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { @@ -93,10 +94,12 @@ protected function setUp() ->getMock(); $this->clientConfig = $this->getMockBuilder(\Magento\Elasticsearch\Model\Config::class) ->disableOriginalConstructor() - ->setMethods([ - 'getIndexPrefix', - 'getEntityType', - ])->getMock(); + ->setMethods( + [ + 'getIndexPrefix', + 'getEntityType', + ] + )->getMock(); $this->indexBuilder = $this->getMockBuilder(\Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -104,44 +107,52 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) - ->setMethods([ - 'indices', - 'ping', - 'bulk', - 'search', - ]) + ->setMethods( + [ + 'indices', + 'ping', + 'bulk', + 'search', + ] + ) ->disableOriginalConstructor() ->getMock(); $indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class) - ->setMethods([ - 'exists', - 'getSettings', - 'create', - 'putMapping', - 'deleteMapping', - 'existsAlias', - 'updateAliases', - 'stats' - ]) + ->setMethods( + [ + 'exists', + 'getSettings', + 'create', + 'putMapping', + 'deleteMapping', + 'existsAlias', + 'updateAliases', + 'stats' + ] + ) ->disableOriginalConstructor() ->getMock(); $elasticsearchClientMock->expects($this->any()) ->method('indices') ->willReturn($indicesMock); $this->client = $this->getMockBuilder(\Magento\Elasticsearch\Model\Client\Elasticsearch::class) - ->setConstructorArgs([ - 'options' => $this->getClientOptions(), - 'elasticsearchClient' => $elasticsearchClientMock - ]) + ->setConstructorArgs( + [ + 'options' => $this->getClientOptions(), + 'elasticsearchClient' => $elasticsearchClientMock + ] + ) ->getMock(); $this->connectionManager->expects($this->any()) ->method('getConnection') ->willReturn($this->client); $this->fieldMapper->expects($this->any()) ->method('getAllAttributesTypes') - ->willReturn([ - 'name' => 'string', - ]); + ->willReturn( + [ + 'name' => 'string', + ] + ); $this->clientConfig->expects($this->any()) ->method('getIndexPrefix') ->willReturn('indexName'); @@ -151,12 +162,14 @@ protected function setUp() $this->indexNameResolver = $this->getMockBuilder( \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver::class ) - ->setMethods([ - 'getIndexName', - 'getIndexNamespace', - 'getIndexFromAlias', - 'getIndexNameForAlias', - ]) + ->setMethods( + [ + 'getIndexName', + 'getIndexNamespace', + 'getIndexFromAlias', + 'getIndexNameForAlias', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->batchDocumentDataMapper = $this->getMockBuilder( @@ -216,9 +229,11 @@ public function testPrepareDocsPerStore() { $this->batchDocumentDataMapper->expects($this->once()) ->method('map') - ->willReturn([ - 'name' => 'Product Name', - ]); + ->willReturn( + [ + 'name' => 'Product Name', + ] + ); $this->assertInternalType( 'array', $this->model->prepareDocsPerStore( @@ -283,10 +298,6 @@ public function testCleanIndex() ->with(1, 'product', []) ->willReturn('indexName_product_1_v'); - $this->client->expects($this->once()) - ->method('isEmptyIndex') - ->with('indexName_product_1_v') - ->willReturn(false); $this->client->expects($this->atLeastOnce()) ->method('indexExists') ->willReturn(true); @@ -299,26 +310,6 @@ public function testCleanIndex() ); } - /** - * Test cleanIndex() method isEmptyIndex is true - */ - public function testCleanIndexTrue() - { - $this->indexNameResolver->expects($this->any()) - ->method('getIndexName') - ->willReturn('indexName_product_1_v'); - - $this->client->expects($this->once()) - ->method('isEmptyIndex') - ->with('indexName_product_1_v') - ->willReturn(true); - - $this->assertSame( - $this->model, - $this->model->cleanIndex(1, 'product') - ); - } - /** * Test deleteDocs() method */ @@ -376,9 +367,11 @@ public function testConnectException() { $connectionManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) ->disableOriginalConstructor() - ->setMethods([ - 'getConnection', - ]) + ->setMethods( + [ + 'getConnection', + ] + ) ->getMock(); $connectionManager->expects($this->any()) diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 55df6a5a37f46..f42d957276d76 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -267,7 +267,7 @@ </type> <virtualType name="Magento\Elasticsearch\Elasticsearch5\SearchAdapter\ConnectionManager" type="Magento\Elasticsearch\SearchAdapter\ConnectionManager"> <arguments> - <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory</argument> + <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy</argument> <argument name="clientConfig" xsi:type="object">Magento\Elasticsearch\Model\Config</argument> </arguments> </virtualType> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..f1f2f39f4457b --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SearchEngineElasticsearchConfigData"> + <data key="path">catalog/search/engine</data> + <data key="scope_id">1</data> + <data key="label">Elasticsearch 6.0+</data> + <data key="value">elasticsearch6</data> + </entity> +</entities> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml new file mode 100644 index 0000000000000..d612f5bd17a2f --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="SearchEngineElasticsearchSuite"> + <before> + <magentoCLI stepKey="setSearchEngineToElasticsearch" command="config:set {{SearchEngineElasticsearchConfigData.path}} {{SearchEngineElasticsearchConfigData.value}}"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after></after> + <include> + <group name="SearchEngineElasticsearch" /> + </include> + <exclude> + <group name="skip"/> + </exclude> + </suite> +</suites> diff --git a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml index 1155930dd75ef..3b99ade32e6ce 100644 --- a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml +++ b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml @@ -21,7 +21,7 @@ <amOnPage url="{{AdminEmailTemplateIndexPage.url}}" stepKey="navigateToEmailTemplatePage"/> <!--Click "Add New Template" button--> <click selector="{{AdminMainActionsSection.add}}" stepKey="clickAddNewTemplateButton"/> - <!--Select value for "Template" drop-down menu in "Load default template" tab--> + <!--Select value for "Template" drop-down menu in "Load Default Template" tab--> <selectOption selector="{{AdminEmailTemplateEditSection.templateDropDown}}" userInput="Registry Update" stepKey="selectValueFromTemplateDropDown"/> <!--Fill in required fields in "Template Information" tab and click "Save Template" button--> <click selector="{{AdminEmailTemplateEditSection.loadTemplateButton}}" stepKey="clickLoadTemplateButton"/> diff --git a/app/code/Magento/Email/i18n/en_US.csv b/app/code/Magento/Email/i18n/en_US.csv index 8eed4b5c662b5..412660d90d469 100644 --- a/app/code/Magento/Email/i18n/en_US.csv +++ b/app/code/Magento/Email/i18n/en_US.csv @@ -72,7 +72,7 @@ City,City "We're sorry, an error has occurred while generating this content.","We're sorry, an error has occurred while generating this content." "Invalid sender data","Invalid sender data" Title,Title -"Load default template","Load default template" +"Load Default Template","Load Default Template" Template,Template "Are you sure you want to strip tags?","Are you sure you want to strip tags?" "Are you sure you want to delete this template?","Are you sure you want to delete this template?" diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index 1f236a21a7306..73d80c0ae4d57 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -12,9 +12,9 @@ use Magento\Framework\App\TemplateTypesInterface; <form action="<?= $block->escapeUrl($block->getLoadUrl()) ?>" method="post" id="email_template_load_form"> <?= $block->getBlockHtml('formkey') ?> <fieldset class="admin__fieldset form-inline"> - <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load default template')) ?></span></legend><br> - <div class="admin__field"> - <label class="admin__field-label" for="template_select"><?= $block->escapeHtml(__('Template')) ?></label> + <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load Default Template')) ?></span></legend><br> + <div class="admin__field required"> + <label class="admin__field-label" for="template_select"><span><?= $block->escapeHtml(__('Template')) ?></span></label> <div class="admin__field-control"> <select id="template_select" name="code" class="admin__control-select required-entry"> <?php foreach ($block->getTemplateOptions() as $group => $options) : ?> diff --git a/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml b/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml new file mode 100644 index 0000000000000..d03403970ae55 --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Data/FedExConfigData.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminFedexEnableForCheckoutConfigData" type="fedex_config"> + <data key="path">carriers/fedex/active</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableSandboxModeConfigData" type="fedex_config"> + <data key="path">carriers/fedex/sandbox_mode</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableDebugConfigData" type="fedex_config"> + <data key="path">carriers/fedex/debug</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexEnableShowMethodConfigData" type="fedex_config"> + <data key="path">carriers/fedex/showmethod</data> + <data key="value">1</data> + <data key="label">Yes</data> + </entity> + <entity name="AdminFedexDisableShowMethodConfigData" type="fedex_config"> + <data key="path">carriers/fedex/showmethod</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableDebugConfigData" type="fedex_config"> + <data key="path">carriers/fedex/debug</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableSandboxModeConfigData" type="fedex_config"> + <data key="path">carriers/fedex/sandbox_mode</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> + <entity name="AdminFedexDisableForCheckoutConfigData" type="fedex_config"> + <data key="path">carriers/fedex/active</data> + <data key="value">0</data> + <data key="label">No</data> + </entity> +</entities> diff --git a/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml new file mode 100644 index 0000000000000..0f75d475d6b1b --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodFedExSection"> + <element name="carriersFedExTab" type="button" selector="#carriers_fedex-head"/> + <element name="carriersFedExActive" type="input" selector="#carriers_fedex_active_inherit"/> + <element name="carriersFedExTitle" type="input" selector="#carriers_fedex_title_inherit"/> + <element name="carriersFedExAccountId" type="input" selector="#carriers_fedex_account"/> + <element name="carriersFedExMeterNumber" type="input" selector="#carriers_fedex_meter_number"/> + <element name="carriersFedExKey" type="input" selector="#carriers_fedex_key"/> + <element name="carriersFedExPassword" type="input" selector="#carriers_fedex_password"/> + <element name="carriersFedExSandboxMode" type="input" selector="#carriers_fedex_sandbox_mode_inherit"/> + <element name="carriersFedExShipmentRequestType" type="input" selector="#carriers_fedex_shipment_requesttype_inherit"/> + <element name="carriersFedExPackaging" type="input" selector="#carriers_fedex_packaging_inherit"/> + <element name="carriersFedExDropoff" type="input" selector="#carriers_fedex_dropoff_inherit"/> + <element name="carriersFedExUnitOfMeasure" type="input" selector="#carriers_fedex_unit_of_measure_inherit"/> + <element name="carriersFedExMaxPackageWeight" type="input" selector="#carriers_fedex_max_package_weight_inherit"/> + <element name="carriersFedExHandlingType" type="input" selector="#carriers_fedex_handling_type_inherit"/> + <element name="carriersFedExHandlingAction" type="select" selector="#carriers_fedex_handling_action_inherit"/> + <element name="carriersFedExFreeMethod" type="input" selector="#carriers_fedex_free_method_inherit"/> + <element name="carriersFedExSpecificErrMsg" type="input" selector="#carriers_fedex_specificerrmsg_inherit"/> + <element name="carriersFedExAllowSpecific" type="input" selector="#carriers_fedex_sallowspecific_inherit"/> + <element name="carriersFedExSpecificCountry" type="input" selector="#carriers_fedex_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..f599d7ca223ae --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in FedEx section--> + <comment userInput="Assert configuration are disabled in FedEx section" stepKey="commentSeeDisabledFedExConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodFedExSection.carriersFedExTab}}" dependentSelector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" visible="false" stepKey="expandFedExTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" stepKey="waitFedExTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExActive}}" userInput="disabled" stepKey="grabFedExActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExActiveDisabled" stepKey="assertFedExActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExTitle}}" userInput="disabled" stepKey="grabFedExTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExTitleDisabled" stepKey="assertFedExTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExAccountId}}" userInput="disabled" stepKey="grabFedExAccountIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExAccountIdDisabled" stepKey="assertFedExAccountIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExMeterNumber}}" userInput="disabled" stepKey="grabFedExMeterNumberDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExMeterNumberDisabled" stepKey="assertFedExMeterNumberDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExKey}}" userInput="disabled" stepKey="grabFedExKeyDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExKeyDisabled" stepKey="assertFedExKeyDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPassword}}" userInput="disabled" stepKey="grabFedExPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExPasswordDisabled" stepKey="assertFedExPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSandboxMode}}" userInput="disabled" stepKey="grabFedExSandboxDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSandboxDisabled" stepKey="assertFedExSandboxDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExShipmentRequestType}}" userInput="disabled" stepKey="grabFedExShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExShipmentRequestTypeDisabled" stepKey="assertFedExShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPackaging}}" userInput="disabled" stepKey="grabFedExPackagingDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExPackagingDisabled" stepKey="assertFedExPackagingDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExDropoff}}" userInput="disabled" stepKey="grabFedExDropoffDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExDropoffDisabled" stepKey="assertFedExDropoffDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExUnitOfMeasure}}" userInput="disabled" stepKey="grabFedExUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExUnitOfMeasureDisabled" stepKey="assertFedExUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExMaxPackageWeight}}" userInput="disabled" stepKey="grabFedExMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExMaxPackageWeightDisabled" stepKey="assertFedExMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExHandlingType}}" userInput="disabled" stepKey="grabFedExHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExHandlingTypeDisabled" stepKey="assertFedExHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExHandlingAction}}" userInput="disabled" stepKey="grabFedExHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExHandlingActionDisabled" stepKey="assertFedExHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSpecificErrMsg}}" userInput="disabled" stepKey="grabFedExSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSpecificErrMsgDisabled" stepKey="assertFedExSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExAllowSpecific}}" userInput="disabled" stepKey="grabFedExAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExAllowSpecificDisabled" stepKey="assertFedExAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSpecificCountry}}" userInput="disabled" stepKey="grabFedExSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFedExSpecificCountryDisabled" stepKey="assertFedExSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml new file mode 100644 index 0000000000000..91a76383babd4 --- /dev/null +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreatingShippingLabelTest"> + <annotations> + <features value="Fedex"/> + <stories value="Shipping label"/> + <title value="Creating shipping label"/> + <description value="Creating shipping label"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20287"/> + <useCaseId value="MC-18215"/> + <group value="shipping"/> + <skip> + <issueId value="MQE-1578"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create product --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Set Fedex configs data--> + <magentoCLI command="config:set {{AdminFedexEnableForCheckoutConfigData.path}} {{AdminFedexEnableForCheckoutConfigData.value}}" stepKey="enableCheckout"/> + <magentoCLI command="config:set {{AdminFedexEnableSandboxModeConfigData.path}} {{AdminFedexEnableSandboxModeConfigData.value}}" stepKey="enableSandbox"/> + <magentoCLI command="config:set {{AdminFedexEnableDebugConfigData.path}} {{AdminFedexEnableDebugConfigData.value}}" stepKey="enableDebug"/> + <magentoCLI command="config:set {{AdminFedexEnableShowMethodConfigData.path}} {{AdminFedexEnableShowMethodConfigData.value}}" stepKey="enableShowMethod"/> + <!--TODO: add fedex credentials--> + <!--Set StoreInformation configs data--> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} '{{AdminGeneralSetStoreNameConfigData.value}}'" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.telephone}}" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setStoreInformationCountry"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} {{AdminGeneralSetVatNumberConfigData.value}}" stepKey="setStoreInformationVatNumber"/> + <!--Set Shipping settings origin data--> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCountryConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="setOriginCountry"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.city}}" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setOriginStreetAddress2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!--Reset configs--> + <magentoCLI command="config:set {{AdminFedexDisableForCheckoutConfigData.path}} {{AdminFedexDisableForCheckoutConfigData.value}}" stepKey="disableCheckout"/> + <magentoCLI command="config:set {{AdminFedexDisableSandboxModeConfigData.path}} {{AdminFedexDisableSandboxModeConfigData.value}}" stepKey="disableSandbox"/> + <magentoCLI command="config:set {{AdminFedexDisableDebugConfigData.path}} {{AdminFedexDisableDebugConfigData.value}}" stepKey="disableDebug"/> + <magentoCLI command="config:set {{AdminFedexDisableShowMethodConfigData.path}} {{AdminFedexDisableShowMethodConfigData.value}}" stepKey="disableShowMethod"/> + <magentoCLI command="config:set {{AdminGeneralSetStoreNameConfigData.path}} ''" stepKey="setStoreInformationName"/> + <magentoCLI command="config:set {{AdminGeneralSetStorePhoneConfigData.path}} ''" stepKey="setStoreInformationPhone"/> + <magentoCLI command="config:set {{AdminGeneralSetCityConfigData.path}} ''" stepKey="setStoreInformationCity"/> + <magentoCLI command="config:set {{AdminGeneralSetPostcodeConfigData.path}} ''" stepKey="setStoreInformationPostcode"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddressConfigData.path}} ''" stepKey="setStoreInformationStreetAddress"/> + <magentoCLI command="config:set {{AdminGeneralSetStreetAddress2ConfigData.path}} ''" stepKey="setStoreInformationStreetAddress2"/> + <magentoCLI command="config:set {{AdminGeneralSetVatNumberConfigData.path}} ''" stepKey="setStoreInformationVatNumber"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginCityConfigData.path}} ''" stepKey="setOriginCity"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} ''" stepKey="setOriginZipCode"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} ''" stepKey="setOriginStreetAddress"/> + <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} ''" stepKey="setOriginStreetAddress2"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Add country of manufacture to product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="amOnEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + <actionGroup ref="AdminFillProductCountryOfManufactureActionGroup" stepKey="fillCountryOfManufacture"> + <argument name="countryId" value="DE"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <!--Place for order using FedEx shipping method--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnStorefrontProductPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="addAddress"> + <argument name="customerVar" value="Simple_US_Utah_Customer"/> + <argument name="customerAddressVar" value="US_Address_California"/> + <argument name="shippingMethod" value="Federal Express"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <!--Open created order in admin--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <!--Create Invoice--> + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createInvoice"/> + <!--Create shipping label--> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipmentIntoOrder"/> + <checkOption selector="{{AdminShipmentTotalSection.createShippingLabel}}" stepKey="checkCreateShippingLabel"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <actionGroup ref="AdminShipmentCreateShippingLabelActionGroup" stepKey="createPackage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AdminGoToShipmentTabActionGroup" stepKey="goToShipmentTab"/> + <click selector="{{AdminOrderShipmentsTabSection.viewGridRow('1')}}" stepKey="clickRowToViewShipment"/> + <waitForPageLoad stepKey="waitForShipmentItemsSection"/> + <seeElement selector="{{AdminShipmentTrackingInformationShippingSection.shippingInfoTable}}" stepKey="seeInformationTable"/> + <seeElement selector="{{AdminShipmentTrackingInformationShippingSection.shippingNumber}}" stepKey="seeShippingNumberElement"/> + <grabTextFrom selector="{{AdminShipmentTrackingInformationShippingSection.shippingMethod}}" stepKey="grabShippingMethod"/> + <grabTextFrom selector="{{AdminShipmentTrackingInformationShippingSection.shippingMethodTitle}}" stepKey="grabShippingMethodTitle"/> + <assertEquals actual="$grabShippingMethod" expectedType="string" expected="Federal Express" stepKey="assertShippingMethodIsFedEx"/> + <assertEquals actual="$grabShippingMethodTitle" expectedType="string" expected="Federal Express" stepKey="assertShippingMethodTitleIsFedEx"/> + </test> +</tests> diff --git a/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php b/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php index e02401c1a865d..59ad900c491b6 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php +++ b/app/code/Magento/GiftMessage/Block/Message/Multishipping/Plugin/ItemsBox.php @@ -43,6 +43,15 @@ public function __construct(MessageHelper $helper) */ public function afterGetItemsBoxTextAfter(ShippingBlock $subject, $itemsBoxText, DataObject $addressEntity) { + if ($addressEntity->getGiftMessageId() === null) { + $addressEntity->setGiftMessageId($addressEntity->getQuote()->getGiftMessageId()); + } + foreach ($addressEntity->getAllItems() as $item) { + if ($item->getGiftMessageId() === null) { + $item->setGiftMessageId($item->getQuoteItem()->getGiftMessageId()); + } + } + return $itemsBoxText . $this->helper->getInline('multishipping_address', $addressEntity); } } diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.xml new file mode 100644 index 0000000000000..dc6d0b79a8367 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/AdminOrderGiftSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderGiftSection"> + <element name="orderItemGiftOptionsLink" type="text" selector="//table[contains(@class, 'edit-order-table')]//tbody[contains(.,'{{productName}}')]//a[contains(@class, 'action-link')]" parameterized="true"/> + <element name="orderItemGiftMessage" type="textarea" selector="#current_item_giftmessage_message" /> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml new file mode 100644 index 0000000000000..b1f6f35ba5d9c --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftMessageSection.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCartGiftMessageSection"> + <element name="giftItemMessage" type="textarea" selector="tbody.cart:nth-of-type({{blockNumber}}) #gift-message-whole-message" parameterized="true"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.xml new file mode 100644 index 0000000000000..e39279de228f9 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontCheckoutCartGiftSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCartGiftSection"> + <element name="cartItemGiftMessage" type="text" selector="//tbody[contains(.,'{{productName}}')]//div[@class='gift-message']//textarea" parameterized="true"/> + <element name="orderNumber" type="text" selector="(//div[contains(@class, 'orders-succeed')]//a)[{{blockNumber}}]" parameterized="true"/> + <element name="viewOrder" type="text" selector="//table[@id='my-orders-table']//tr[contains(.,'{{orderNumber}}')]//a[contains(@class, 'action view')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.xml new file mode 100644 index 0000000000000..45e7531f0b4a8 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/StorefrontOrderGiftSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontOrderGiftSection"> + <element name="giftMessageLink" type="button" selector=".table-wrapper.order-items .options .action.show"/> + <element name="giftMessage" type="text" selector=".order-gift-message .item-message" /> + </section> +</sections> diff --git a/app/code/Magento/GiftMessage/etc/db_schema.xml b/app/code/Magento/GiftMessage/etc/db_schema.xml index 4ae98799df0c2..5c1e7fb17bc5d 100644 --- a/app/code/Magento/GiftMessage/etc/db_schema.xml +++ b/app/code/Magento/GiftMessage/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="gift_message" resource="default" engine="innodb" comment="Gift Message"> <column xsi:type="int" name="gift_message_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="GiftMessage Id"/> + comment="GiftMessage ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer id"/> + default="0" comment="Customer ID"/> <column xsi:type="varchar" name="sender" nullable="true" length="255" comment="Sender"/> <column xsi:type="varchar" name="recipient" nullable="true" length="255" comment="Registrant"/> <column xsi:type="text" name="message" nullable="true" comment="Message"/> @@ -21,27 +21,27 @@ </table> <table name="quote" resource="checkout" comment="Sales Flat Quote"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_address" resource="checkout" comment="Sales Flat Quote Address"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_item" resource="checkout" comment="Sales Flat Quote Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="quote_address_item" resource="checkout" comment="Sales Flat Quote Address Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="sales_order" resource="sales" comment="Sales Flat Order"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> </table> <table name="sales_order_item" resource="sales" comment="Sales Flat Order Item"> <column xsi:type="int" name="gift_message_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Gift Message Id"/> + comment="Gift Message ID"/> <column xsi:type="int" name="gift_message_available" padding="11" unsigned="false" nullable="true" identity="false" comment="Gift Message Available"/> </table> diff --git a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml index 76c377544cfb3..ed3dfcecb90f3 100644 --- a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml +++ b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml @@ -9,12 +9,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="googleoptimizer_code" resource="default" engine="innodb" comment="Google Experiment code"> <column xsi:type="int" name="code_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Google experiment code id"/> + comment="Google experiment code ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Optimized entity id product id or catalog id"/> + comment="Optimized entity ID product ID or catalog ID"/> <column xsi:type="varchar" name="entity_type" nullable="true" length="50" comment="Optimized entity type"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store id"/> + comment="Store ID"/> <column xsi:type="text" name="experiment_script" nullable="true" comment="Google experiment script"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="code_id"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml index ba3703e7b0edc..e6d7588289c39 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -28,6 +28,17 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiGroupedProductAndUnderscoredSku" 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> <entity name="ApiGroupedProduct2" type="product3"> <data key="sku" unique="suffix">apiGroupedProduct</data> <data key="type_id">grouped</data> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 5d65f82690235..f2cb2cc993a50 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -29,6 +29,9 @@ </createData> </before> <after> + <actionGroup ref="deleteProductBySku" stepKey="deleteGroupedProduct"> + <argument name="sku" value="{{GroupedProduct.sku}}"/> + </actionGroup> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml index 2a600d38250f8..4fd06ccaa27ec 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml @@ -17,6 +17,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-141"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByNameMysqlTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product name using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product name using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20464"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -51,7 +87,7 @@ <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> - <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="ApiGroupedProductAndUnderscoredSku" stepKey="product"/> <createData entity="OneSimpleProductLink" stepKey="addProductOne"> <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple1"/> @@ -77,6 +113,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-282"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByDescriptionTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product description using the MYSQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20468"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -107,6 +179,42 @@ <severity value="MAJOR"/> <testCaseId value="MC-283"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByShortDescriptionMysqlTest" extends="AdvanceCatalogSearchSimpleProductByShortDescriptionTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product short description using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product short description using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20469"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -137,6 +245,51 @@ <severity value="MAJOR"/> <testCaseId value="MC-284"/> <group value="GroupedProduct"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <getData entity="GetProduct3" stepKey="arg1"> + <requiredEntity createDataKey="product"/> + </getData> + <getData entity="GetProduct" stepKey="arg2"> + <requiredEntity createDataKey="simple1"/> + </getData> + <getData entity="GetProduct" stepKey="arg3"> + <requiredEntity createDataKey="simple2"/> + </getData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + <see userInput="3 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="see"/> + <see userInput="$$product.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('1')}}" stepKey="seeProductName"/> + <see userInput="$$simple1.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('2')}}" stepKey="seeSimple1ProductName"/> + <see userInput="$$simple2.name$$" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.nthProductName('3')}}" stepKey="seeSimple2ProductName"/> + </test> + <test name="AdvanceCatalogSearchGroupedProductByPriceMysqlTest" extends="AdvanceCatalogSearchSimpleProductByPriceTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product price using the MySQL search engine"/> + <description value="Guest customer should be able to advance search Grouped product with product price using the MySQL search engine"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20470"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest new file mode 100644 index 0000000000000..5220349a4aac3 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest" extends="AdvanceCatalogSearchSimpleProductBySkuTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Advanced Catalog Product Search for all product types"/> + <title value="Guest customer should be able to advance search Grouped product with product sku that in camelCase format"/> + <description value="Guest customer should be able to advance search Grouped product with product sku that in camelCase format"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20519"/> + <group value="GroupedProduct"/> + <group value="SearchEngineMysql"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + <createData entity="ApiGroupedProduct" stepKey="product"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple1"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addProductOne" stepKey="addProductTwo"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="simple2"/> + </updateData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php new file mode 100644 index 0000000000000..b2336a0741292 --- /dev/null +++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProductGraphQl\Model\Resolver\Product\Price; + +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\Pricing\PriceInfoInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; + +/** + * Provides product prices for configurable products + */ +class Provider implements ProviderInterface +{ + /** + * Cache product prices so only fetch once + * + * @var AmountInterface[] + */ + private $minimalProductAmounts; + + /** + * @inheritdoc + */ + public function getMinimalFinalPrice(SaleableInterface $product): AmountInterface + { + return $this->getMinimalProductAmount($product, FinalPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface + { + return $this->getMinimalProductAmount($product, RegularPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface + { + //Use minimal for maximal since maximal price in infinite + return $this->getMinimalProductAmount($product, FinalPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface + { + //Use minimal for maximal since maximal price in infinite + return $this->getMinimalProductAmount($product, RegularPrice::PRICE_CODE); + } + + /** + * @inheritdoc + */ + public function getRegularPrice(SaleableInterface $product): AmountInterface + { + return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount(); + } + + /** + * Get minimal amount for cheapest product in group + * + * @param SaleableInterface $product + * @param string $priceType + * @return AmountInterface + */ + private function getMinimalProductAmount(SaleableInterface $product, string $priceType): AmountInterface + { + if (empty($this->minimalProductAmounts[$product->getId()][$priceType])) { + $products = $product->getTypeInstance()->getAssociatedProducts($product); + $minPrice = null; + foreach ($products as $item) { + $item->setQty(PriceInfoInterface::PRODUCT_QUANTITY_DEFAULT); + $price = $item->getPriceInfo()->getPrice($priceType); + $priceValue = $price->getValue(); + if (($priceValue !== false) && ($priceValue <= ($minPrice === null ? $priceValue : $minPrice))) { + $minPrice = $price->getValue(); + $this->minimalProductAmounts[$product->getId()][$priceType] = $price->getAmount(); + } + } + } + + return $this->minimalProductAmounts[$product->getId()][$priceType]; + } +} diff --git a/app/code/Magento/GroupedProductGraphQl/composer.json b/app/code/Magento/GroupedProductGraphQl/composer.json index cd22c6066eb4a..9578aa27ba180 100644 --- a/app/code/Magento/GroupedProductGraphQl/composer.json +++ b/app/code/Magento/GroupedProductGraphQl/composer.json @@ -5,6 +5,7 @@ "require": { "php": "~7.1.3||~7.2.0||~7.3.0", "magento/module-grouped-product": "*", + "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*", "magento/framework": "*" }, diff --git a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml index 5c41fc26e615b..4c408b38b5ec3 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml @@ -29,4 +29,12 @@ </argument> </arguments> </type> + + <type name="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="grouped" xsi:type="object">Magento\GroupedProductGraphQl\Model\Resolver\Product\Price\Provider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php new file mode 100644 index 0000000000000..aa14d562d9cf7 --- /dev/null +++ b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Api\Data; + +/** + * Extended export interface for implementation of Skipped Attributes which are missing from the basic interface + */ +interface ExtendedExportInfoInterface extends ExportInfoInterface +{ + /** + * Returns skipped attributes + * + * @return mixed + */ + public function getSkipAttr(); + + /** + * Set skipped attributes + * + * @param string $skipAttr + * @return mixed + */ + public function setSkipAttr($skipAttr); +} diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php index 13c22a976e798..c5885f72474f9 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php @@ -5,13 +5,13 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Export; +use Magento\Backend\App\Action\Context; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\MessageQueue\PublisherInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; -use Magento\Backend\App\Action\Context; -use Magento\Framework\App\Response\Http\FileFactory; use Magento\ImportExport\Model\Export as ExportModel; -use Magento\Framework\MessageQueue\PublisherInterface; use Magento\ImportExport\Model\Export\Entity\ExportInfoFactory; /** @@ -76,11 +76,16 @@ public function execute() try { $params = $this->getRequest()->getParams(); + if (!array_key_exists('skip_attr', $params)) { + $params['skip_attr'] = []; + } + /** @var ExportInfoFactory $dataObject */ $dataObject = $this->exportInfoFactory->create( $params['file_format'], $params['entity'], - $params['export_filter'] + $params['export_filter'], + $params['skip_attr'] ); $this->messagePublisher->publish('import_export.export', $dataObject); diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php index 6dffc1827cfd0..a5d5d63e4f8da 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php @@ -7,12 +7,12 @@ namespace Magento\ImportExport\Model\Export\Entity; -use \Magento\ImportExport\Api\Data\ExportInfoInterface; +use Magento\ImportExport\Api\Data\ExtendedExportInfoInterface; /** * Class ExportInfo implementation for ExportInfoInterface. */ -class ExportInfo implements ExportInfoInterface +class ExportInfo implements ExtendedExportInfoInterface { /** * @var string @@ -39,6 +39,11 @@ class ExportInfo implements ExportInfoInterface */ private $exportFilter; + /** + * @var mixed + */ + private $skipAttr; + /** * @inheritdoc */ @@ -118,4 +123,20 @@ public function setExportFilter($exportFilter) { $this->exportFilter = $exportFilter; } + + /** + * @inheritdoc + */ + public function getSkipAttr() + { + return $this->skipAttr; + } + + /** + * @inheritdoc + */ + public function setSkipAttr($skipAttr) + { + $this->skipAttr = $skipAttr; + } } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php index e3cbd162aa5af..32c989acb661c 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php @@ -84,17 +84,25 @@ public function __construct( * @param string $fileFormat * @param string $entity * @param string $exportFilter + * @param array $skipAttr * @return ExportInfoInterface * @throws \Magento\Framework\Exception\LocalizedException */ - public function create($fileFormat, $entity, $exportFilter) + public function create($fileFormat, $entity, $exportFilter, $skipAttr) { $writer = $this->getWriter($fileFormat); - $entityAdapter = $this->getEntityAdapter($entity, $fileFormat, $exportFilter, $writer->getContentType()); + $entityAdapter = $this->getEntityAdapter( + $entity, + $fileFormat, + $exportFilter, + $skipAttr, + $writer->getContentType() + ); $fileName = $this->generateFileName($entity, $entityAdapter, $writer->getFileExtension()); /** @var ExportInfoInterface $exportInfo */ $exportInfo = $this->objectManager->create(ExportInfoInterface::class); $exportInfo->setExportFilter($this->serializer->serialize($exportFilter)); + $exportInfo->setSkipAttr($skipAttr); $exportInfo->setFileName($fileName); $exportInfo->setEntity($entity); $exportInfo->setFileFormat($fileFormat); @@ -130,11 +138,12 @@ private function generateFileName($entity, $entityAdapter, $fileExtensions) * @param string $entity * @param string $fileFormat * @param string $exportFilter + * @param array $skipAttr * @param string $contentType * @return \Magento\ImportExport\Model\Export\AbstractEntity|AbstractEntity * @throws \Magento\Framework\Exception\LocalizedException */ - private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentType) + private function getEntityAdapter($entity, $fileFormat, $exportFilter, $skipAttr, $contentType) { $entities = $this->exportConfig->getEntities(); if (isset($entities[$entity])) { @@ -166,12 +175,15 @@ private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentT } else { throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity.')); } - $entityAdapter->setParameters([ - 'fileFormat' => $fileFormat, - 'entity' => $entity, - 'exportFilter' => $exportFilter, - 'contentType' => $contentType, - ]); + $entityAdapter->setParameters( + [ + 'fileFormat' => $fileFormat, + 'entity' => $entity, + 'exportFilter' => $exportFilter, + 'skipAttr' => $skipAttr, + 'contentType' => $contentType, + ] + ); return $entityAdapter; } diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml index 6bcca6c86c98c..9063916e9f502 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -46,4 +46,18 @@ <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="{{validationNoticeMessage}}" after="waitForValidationNoticeMessage" stepKey="seeValidationNoticeMessage"/> <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="{{validationMessage}}" after="seeValidationNoticeMessage" stepKey="seeValidationMessage"/> </actionGroup> + <actionGroup name="AdminCheckDataForImportProductActionGroup"> + <arguments> + <argument name="behavior" type="string" defaultValue="Add/Update"/> + <argument name="importFile" type="string"/> + </arguments> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <waitForPageLoad stepKey="adminImportMainSectionLoad"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportBehaviorOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml index d44b93bf05c94..ba1deeebbd89a 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml @@ -13,6 +13,8 @@ <element name="importBehavior" type="select" selector="#basic_behavior"/> <element name="selectFileToImport" type="input" selector="#import_file"/> <element name="importButton" type="button" selector="#import_validation_container button" timeout="30"/> + <element name="messageSuccess" type="text" selector=".messages div.message-success"/> + <element name="messageError" type="text" selector=".messages div.message-error"/> <element name="validationStrategy" type="select" selector="#basic_behaviorvalidation_strategy"/> <element name="allowedErrorsCount" type="input" selector="#basic_behavior_allowed_error_count"/> </section> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml new file mode 100644 index 0000000000000..eb84929ec8d93 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductImportCSVFileCorrectDifferentFilesTest"> + <annotations> + <description value="Product import from CSV file correct from different files."/> + <features value="Import/Export"/> + <title value="Product import from CSV file correct from different files."/> + <severity value="MAJOR"/> + <testCaseId value="MC-17104"/> + <useCaseId value="MAGETWO-70803"/> + <group value="importExport"/> + </annotations> + <before> + <!--Login as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Logout from Admin--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Check data products with add/update behavior--> + <actionGroup ref="AdminCheckDataForImportProductActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="BB-ProductsWorking.csv"/> + </actionGroup> + <see selector="{{AdminImportMainSection.messageSuccess}}" userInput='File is valid! To start import process press "Import" button' stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminCheckDataForImportProductActionGroup" stepKey="adminImportProducts1"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="BB-Products.csv"/> + </actionGroup> + <see selector="{{AdminImportMainSection.messageError}}" userInput='Curly quotes used instead of straight quotes in row(s): 84, 85' stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 50e71512c3d28..04a15179477d8 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -640,6 +640,9 @@ public function testGetUnknownEntity($entity) $import->getEntity(); } + /** + * @return array + */ public function unknownEntitiesProvider() { return [ diff --git a/app/code/Magento/ImportExport/etc/communication.xml b/app/code/Magento/ImportExport/etc/communication.xml index 7794b3e5ab248..3f87eef1ddbd4 100644 --- a/app/code/Magento/ImportExport/etc/communication.xml +++ b/app/code/Magento/ImportExport/etc/communication.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> - <topic name="import_export.export" request="Magento\ImportExport\Api\Data\ExportInfoInterface"> + <topic name="import_export.export" request="Magento\ImportExport\Api\Data\ExtendedExportInfoInterface"> <handler name="exportProcessor" type="Magento\ImportExport\Model\Export\Consumer" method="process" /> </topic> </config> diff --git a/app/code/Magento/ImportExport/etc/db_schema.xml b/app/code/Magento/ImportExport/etc/db_schema.xml index df45131848519..404999cb9e07a 100644 --- a/app/code/Magento/ImportExport/etc/db_schema.xml +++ b/app/code/Magento/ImportExport/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="importexport_importdata" resource="default" engine="innodb" comment="Import Data Table"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="varchar" name="entity" nullable="false" length="50" comment="Entity"/> <column xsi:type="varchar" name="behavior" nullable="false" length="10" default="append" comment="Behavior"/> <column xsi:type="longtext" name="data" nullable="true" comment="Data"/> @@ -18,7 +18,7 @@ </table> <table name="import_history" resource="default" engine="innodb" comment="Import history table"> <column xsi:type="int" name="history_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="History record Id"/> + comment="History record ID"/> <column xsi:type="timestamp" name="started_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Started at"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index 909b526e4790c..2a9e1d388754f 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -11,6 +11,7 @@ <preference for="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface" type="Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator" /> <preference for="Magento\ImportExport\Model\Report\ReportProcessorInterface" type="Magento\ImportExport\Model\Report\Csv" /> <preference for="Magento\ImportExport\Api\Data\ExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> + <preference for="Magento\ImportExport\Api\Data\ExtendedExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\ExportManagementInterface" type="Magento\ImportExport\Model\Export\ExportManagement" /> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> diff --git a/app/code/Magento/Indexer/etc/db_schema.xml b/app/code/Magento/Indexer/etc/db_schema.xml index d7cb006a2cf45..c9c8e665b3755 100644 --- a/app/code/Magento/Indexer/etc/db_schema.xml +++ b/app/code/Magento/Indexer/etc/db_schema.xml @@ -9,8 +9,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="indexer_state" resource="default" engine="innodb" comment="Indexer State"> <column xsi:type="int" name="state_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Indexer State Id"/> - <column xsi:type="varchar" name="indexer_id" nullable="true" length="255" comment="Indexer Id"/> + comment="Indexer State ID"/> + <column xsi:type="varchar" name="indexer_id" nullable="true" length="255" comment="Indexer ID"/> <column xsi:type="varchar" name="status" nullable="true" length="16" default="invalid" comment="Indexer Status"/> <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="Indexer Status"/> @@ -24,13 +24,13 @@ </table> <table name="mview_state" resource="default" engine="innodb" comment="View State"> <column xsi:type="int" name="state_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="View State Id"/> - <column xsi:type="varchar" name="view_id" nullable="true" length="255" comment="View Id"/> + comment="View State ID"/> + <column xsi:type="varchar" name="view_id" nullable="true" length="255" comment="View ID"/> <column xsi:type="varchar" name="mode" nullable="true" length="16" default="disabled" comment="View Mode"/> <column xsi:type="varchar" name="status" nullable="true" length="16" default="idle" comment="View Status"/> <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="View updated time"/> <column xsi:type="int" name="version_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="View Version Id"/> + comment="View Version ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="state_id"/> </constraint> diff --git a/app/code/Magento/Integration/Helper/Oauth/Data.php b/app/code/Magento/Integration/Helper/Oauth/Data.php index de074055efa2a..107583a9e70a8 100644 --- a/app/code/Magento/Integration/Helper/Oauth/Data.php +++ b/app/code/Magento/Integration/Helper/Oauth/Data.php @@ -116,22 +116,22 @@ public function getConsumerPostTimeout() /** * Get customer token lifetime from config. * - * @return int hours + * @return float hours */ public function getCustomerTokenLifetime() { - $hours = (int)$this->_scopeConfig->getValue('oauth/access_token_lifetime/customer'); - return $hours > 0 ? $hours : 0; + $hours = $this->_scopeConfig->getValue('oauth/access_token_lifetime/customer'); + return is_numeric($hours) && $hours > 0 ? $hours : 0; } /** * Get customer token lifetime from config. * - * @return int hours + * @return float hours */ public function getAdminTokenLifetime() { - $hours = (int)$this->_scopeConfig->getValue('oauth/access_token_lifetime/admin'); - return $hours > 0 ? $hours : 0; + $hours = $this->_scopeConfig->getValue('oauth/access_token_lifetime/admin'); + return is_numeric($hours) && $hours > 0 ? $hours : 0; } } diff --git a/app/code/Magento/Integration/etc/db_schema.xml b/app/code/Magento/Integration/etc/db_schema.xml index cbf43d79b2cf6..de0cec2e4e20d 100644 --- a/app/code/Magento/Integration/etc/db_schema.xml +++ b/app/code/Magento/Integration/etc/db_schema.xml @@ -129,7 +129,7 @@ <table name="oauth_token_request_log" resource="default" engine="innodb" comment="Log of token request authentication failures."> <column xsi:type="int" name="log_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Log Id"/> + comment="Log ID"/> <column xsi:type="varchar" name="user_name" nullable="false" length="255" comment="Customer email or admin login"/> <column xsi:type="smallint" name="user_type" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml index 721942f58f7cc..6d182d0b7a5e2 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6092"/> <group value="LayeredNavigation"/> + <group value="SearchEngineMysql"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php index f0243784dd618..34627fbb286ed 100644 --- a/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php +++ b/app/code/Magento/LayeredNavigation/Test/Unit/Block/NavigationTest.php @@ -100,7 +100,7 @@ public function testCanShowBlock() ->method('isEnabled') ->with($this->catalogLayerMock, $filters) ->will($this->returnValue($enabled)); - + $category = $this->createMock(Category::class); $this->catalogLayerMock->expects($this->atLeastOnce())->method('getCurrentCategory')->willReturn($category); $category->expects($this->once())->method('getDisplayMode')->willReturn(Category::DM_PRODUCT); @@ -119,12 +119,12 @@ public function testCanShowBlock() public function testCanShowBlockWithDifferentDisplayModes(string $mode, bool $result) { $filters = ['To' => 'be', 'or' => 'not', 'to' => 'be']; - + $this->filterListMock->expects($this->atLeastOnce())->method('getFilters') ->with($this->catalogLayerMock) ->will($this->returnValue($filters)); $this->assertEquals($filters, $this->model->getFilters()); - + $this->visibilityFlagMock ->expects($this->any()) ->method('isEnabled') @@ -137,6 +137,9 @@ public function testCanShowBlockWithDifferentDisplayModes(string $mode, bool $re $this->assertEquals($result, $this->model->canShowBlock()); } + /** + * @return array + */ public function canShowBlockDataProvider() { return [ diff --git a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml index 0f6e7f93aea11..2c7219fe8afaa 100644 --- a/app/code/Magento/MediaStorage/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaStorage/etc/adminhtml/system.xml @@ -28,6 +28,7 @@ </field> <field id="configuration_update_time" translate="label" type="text" sortOrder="400" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Environment Update Time</label> + <validate>validate-zero-or-greater validate-digits</validate> </field> </group> </section> diff --git a/app/code/Magento/Multishipping/Block/Checkout/Shipping.php b/app/code/Magento/Multishipping/Block/Checkout/Shipping.php index 77981c736b9e9..99450fc538070 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Shipping.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Shipping.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Block\Checkout; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Address; +use Magento\Store\Model\ScopeInterface; /** * Mustishipping checkout shipping @@ -67,6 +70,8 @@ public function getCheckout() } /** + * Add page title and prepare layout + * * @return $this */ protected function _prepareLayout() @@ -78,6 +83,8 @@ protected function _prepareLayout() } /** + * Retrieves addresses + * * @return Address[] */ public function getAddresses() @@ -86,6 +93,8 @@ public function getAddresses() } /** + * Returns count of addresses + * * @return mixed */ public function getAddressCount() @@ -99,6 +108,8 @@ public function getAddressCount() } /** + * Retrieves the address items + * * @param Address $address * @return \Magento\Framework\DataObject[] */ @@ -106,7 +117,7 @@ public function getAddressItems($address) { $items = []; foreach ($address->getAllItems() as $item) { - if ($item->getParentItemId()) { + if ($item->getParentItemId() || !$item->getQuoteItemId()) { continue; } $item->setQuoteItem($this->getCheckout()->getQuote()->getItemById($item->getQuoteItemId())); @@ -118,6 +129,8 @@ public function getAddressItems($address) } /** + * Retrieves the address shipping method + * * @param Address $address * @return mixed */ @@ -127,6 +140,8 @@ public function getAddressShippingMethod($address) } /** + * Retrieves address shipping rates + * * @param Address $address * @return mixed */ @@ -137,22 +152,20 @@ public function getShippingRates($address) } /** + * Retrieves the carrier name by the code + * * @param string $carrierCode * @return string */ public function getCarrierName($carrierCode) { - if ($name = $this->_scopeConfig->getValue( - 'carriers/' . $carrierCode . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ) { - return $name; - } - return $carrierCode; + $name = $this->_scopeConfig->getValue('carriers/' . $carrierCode . '/title', ScopeInterface::SCOPE_STORE); + return $name ?: $carrierCode; } /** + * Retrieves the address edit url + * * @param Address $address * @return string */ @@ -162,6 +175,8 @@ public function getAddressEditUrl($address) } /** + * Retrieves the url for items edition + * * @return string */ public function getItemsEditUrl() @@ -170,6 +185,8 @@ public function getItemsEditUrl() } /** + * Retrieves the url for the post action + * * @return string */ public function getPostActionUrl() @@ -178,6 +195,8 @@ public function getPostActionUrl() } /** + * Retrieves the back url + * * @return string */ public function getBackUrl() @@ -186,6 +205,8 @@ public function getBackUrl() } /** + * Returns converted and formatted price + * * @param Address $address * @param float $price * @param bool $flag @@ -202,7 +223,7 @@ public function getShippingPrice($address, $price, $flag) } /** - * Retrieve text for items box + * Retrieves text for items box * * @param \Magento\Framework\DataObject $addressEntity * @return string diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php b/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php index c86caec733a17..38a30c1ee49e1 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Address/NewShipping.php @@ -1,12 +1,19 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Multishipping\Controller\Checkout\Address; -class NewShipping extends \Magento\Multishipping\Controller\Checkout\Address +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Multishipping\Controller\Checkout\Address; + +/** + * Class NewShipping + * + * @package Address + */ +class NewShipping extends Address implements HttpGetActionInterface { /** * Create New Shipping address Form @@ -35,7 +42,7 @@ public function execute() if ($this->_getCheckout()->getCustomerDefaultShippingAddress()) { $addressForm->setBackUrl($this->_url->getUrl('*/checkout/addresses')); } else { - $addressForm->setBackUrl($this->_url->getUrl('*/cart/')); + $addressForm->setBackUrl($this->_url->getUrl('checkout/cart/')); } } $this->_view->renderLayout(); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php b/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php deleted file mode 100644 index f88cdfc26fa9f..0000000000000 --- a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Multishipping\Controller\Checkout; - -/** - * Turns Off Multishipping mode for Quote. - */ -class Plugin -{ - /** - * @var \Magento\Checkout\Model\Cart - */ - protected $cart; - - /** - * @param \Magento\Checkout\Model\Cart $cart - */ - public function __construct(\Magento\Checkout\Model\Cart $cart) - { - $this->cart = $cart; - } - - /** - * Disable multishipping - * - * @param \Magento\Framework\App\Action\Action $subject - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeExecute(\Magento\Framework\App\Action\Action $subject) - { - $quote = $this->cart->getQuote(); - if ($quote->getIsMultiShipping()) { - $quote->setIsMultiShipping(0); - $this->cart->saveQuote(); - } - } -} diff --git a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php index 4ef36a7c8b6fc..b450232395b88 100644 --- a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php +++ b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php @@ -3,34 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Model\Cart\Controller; +use Magento\Checkout\Controller\Cart; +use Magento\Checkout\Model\Session; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; + +/** + * Cleans shipping addresses and item assignments after MultiShipping flow + */ class CartPlugin { /** - * @var \Magento\Quote\Api\CartRepositoryInterface + * @var CartRepositoryInterface */ private $cartRepository; /** - * @var \Magento\Checkout\Model\Session + * @var Session */ private $checkoutSession; /** - * @var \Magento\Customer\Api\AddressRepositoryInterface + * @var AddressRepositoryInterface */ private $addressRepository; /** - * @param \Magento\Quote\Api\CartRepositoryInterface $cartRepository - * @param \Magento\Checkout\Model\Session $checkoutSession - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository + * @param CartRepositoryInterface $cartRepository + * @param Session $checkoutSession + * @param AddressRepositoryInterface $addressRepository */ public function __construct( - \Magento\Quote\Api\CartRepositoryInterface $cartRepository, - \Magento\Checkout\Model\Session $checkoutSession, - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository + CartRepositoryInterface $cartRepository, + Session $checkoutSession, + AddressRepositoryInterface $addressRepository ) { $this->cartRepository = $cartRepository; $this->checkoutSession = $checkoutSession; @@ -38,20 +52,19 @@ public function __construct( } /** - * @param \Magento\Checkout\Controller\Cart $subject - * @param \Magento\Framework\App\RequestInterface $request + * Cleans shipping addresses and item assignments after MultiShipping flow + * + * @param Cart $subject + * @param RequestInterface $request * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws LocalizedException */ - public function beforeDispatch( - \Magento\Checkout\Controller\Cart $subject, - \Magento\Framework\App\RequestInterface $request - ) { - /** @var \Magento\Quote\Model\Quote $quote */ + public function beforeDispatch(Cart $subject, RequestInterface $request) + { + /** @var Quote $quote */ $quote = $this->checkoutSession->getQuote(); - - // Clear shipping addresses and item assignments after MultiShipping flow - if ($quote->isMultipleShippingAddresses()) { + if ($quote->isMultipleShippingAddresses() && $this->isCheckoutComplete()) { foreach ($quote->getAllShippingAddresses() as $address) { $quote->removeAddress($address->getId()); } @@ -59,12 +72,20 @@ public function beforeDispatch( $shippingAddress = $quote->getShippingAddress(); $defaultShipping = $quote->getCustomer()->getDefaultShipping(); if ($defaultShipping) { - $defaultCustomerAddress = $this->addressRepository->getById( - $defaultShipping - ); + $defaultCustomerAddress = $this->addressRepository->getById($defaultShipping); $shippingAddress->importCustomerAddressData($defaultCustomerAddress); } $this->cartRepository->save($quote); } } + + /** + * Checks whether the checkout flow is complete + * + * @return bool + */ + private function isCheckoutComplete() : bool + { + return (bool) ($this->checkoutSession->getStepData(State::STEP_SHIPPING)['is_complete'] ?? true); + } } diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 7105fd4e9d26d..d1103abfbb94e 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -691,6 +691,19 @@ protected function _prepareOrder(\Magento\Quote\Model\Quote\Address $address) $this->quoteAddressToOrder->convert($address) ); + $shippingMethodCode = $address->getShippingMethod(); + if (isset($shippingMethodCode) && !empty($shippingMethodCode)) { + $rate = $address->getShippingRateByCode($shippingMethodCode); + $shippingPrice = $rate->getPrice(); + } else { + $shippingPrice = $order->getShippingAmount(); + } + $store = $order->getStore(); + $amountPrice = $store->getBaseCurrency() + ->convert($shippingPrice, $store->getCurrentCurrencyCode()); + $order->setBaseShippingAmount($shippingPrice); + $order->setShippingAmount($amountPrice); + $order->setQuote($quote); $order->setBillingAddress($this->quoteAddressToOrderAddress->convert($quote->getBillingAddress())); diff --git a/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php new file mode 100644 index 0000000000000..fff2346d76240 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Checkout\Model\Cart; +use Magento\Framework\App\Action\Action; + +/** + * Turns Off Multishipping mode for Quote. + */ +class DisableMultishippingMode +{ + /** + * @var Cart + */ + private $cart; + + /** + * @param Cart $cart + */ + public function __construct( + Cart $cart + ) { + $this->cart = $cart; + } + + /** + * Disable multishipping + * + * @param Action $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(Action $subject) + { + $quote = $this->cart->getQuote(); + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $extensionAttributes = $quote->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $extensionAttributes->setShippingAssignments([]); + } + $this->cart->saveQuote(); + } + } +} diff --git a/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php new file mode 100644 index 0000000000000..af19e4bc91f51 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Framework\Api\SearchResultsInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor; +use Magento\Quote\Model\ShippingAssignmentFactory; + +/** + * Plugin for multishipping quote processing. + */ +class MultishippingQuoteRepository +{ + /** + * @var ShippingAssignmentFactory + */ + private $shippingAssignmentFactory; + + /** + * @var ShippingProcessor + */ + private $shippingProcessor; + + /** + * @param ShippingAssignmentFactory $shippingAssignmentFactory + * @param ShippingProcessor $shippingProcessor + */ + public function __construct( + ShippingAssignmentFactory $shippingAssignmentFactory, + ShippingProcessor $shippingProcessor + ) { + $this->shippingAssignmentFactory = $shippingAssignmentFactory; + $this->shippingProcessor = $shippingProcessor; + } + + /** + * Process multishipping quote for get. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $result + * @return CartInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGet( + CartRepositoryInterface $subject, + CartInterface $result + ) { + return $this->processQuote($result); + } + + /** + * Process multishipping quote for get list. + * + * @param CartRepositoryInterface $subject + * @param SearchResultsInterface $result + * + * @return SearchResultsInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetList( + CartRepositoryInterface $subject, + SearchResultsInterface $result + ) { + $items = []; + foreach ($result->getItems() as $item) { + $items[] = $this->processQuote($item); + } + $result->setItems($items); + + return $result; + } + + /** + * Remove shipping assignments for multishipping quote. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $quote + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote) + { + $extensionAttributes = $quote->getExtensionAttributes(); + if ($quote->getIsMultiShipping() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $quote->getExtensionAttributes()->setShippingAssignments([]); + } + + return [$quote]; + } + + /** + * Set shipping assignments for multishipping quote according to customer selection. + * + * @param CartInterface $quote + * @return CartInterface + */ + private function processQuote(CartInterface $quote): CartInterface + { + if (!$quote->getIsMultiShipping() || !$quote instanceof Quote) { + return $quote; + } + + if ($quote->getExtensionAttributes() && $quote->getExtensionAttributes()->getShippingAssignments()) { + $shippingAssignments = []; + $addresses = $quote->getAllAddresses(); + + foreach ($addresses as $address) { + $quoteItems = $this->getQuoteItems($quote, $address); + if (!empty($quoteItems)) { + $shippingAssignment = $this->shippingAssignmentFactory->create(); + $shippingAssignment->setItems($quoteItems); + $shippingAssignment->setShipping($this->shippingProcessor->create($address)); + $shippingAssignments[] = $shippingAssignment; + } + } + + if (!empty($shippingAssignments)) { + $quote->getExtensionAttributes()->setShippingAssignments($shippingAssignments); + } + } + + return $quote; + } + + /** + * Returns quote items assigned to address. + * + * @param Quote $quote + * @param Quote\Address $address + * @return Quote\Item[] + */ + private function getQuoteItems(Quote $quote, Quote\Address $address): array + { + $quoteItems = []; + foreach ($address->getItemsCollection() as $addressItem) { + $quoteItem = $quote->getItemById($addressItem->getQuoteItemId()); + if ($quoteItem) { + $multishippingQuoteItem = clone $quoteItem; + $qty = $addressItem->getQty(); + $sku = $multishippingQuoteItem->getSku(); + if (isset($quoteItems[$sku])) { + $qty += $quoteItems[$sku]->getQty(); + } + $multishippingQuoteItem->setQty($qty); + $quoteItems[$sku] = $multishippingQuoteItem; + } + } + + return array_values($quoteItems); + } +} diff --git a/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php new file mode 100644 index 0000000000000..deac19e23a23a --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; + +/** + * Resets quote shipping assignments when item is removed from multishipping quote. + */ +class ResetShippingAssigment +{ + /** + * @var ShippingAssignmentProcessor + */ + private $shippingAssignmentProcessor; + + /** + * @param ShippingAssignmentProcessor $shippingAssignmentProcessor + */ + public function __construct( + ShippingAssignmentProcessor $shippingAssignmentProcessor + ) { + $this->shippingAssignmentProcessor = $shippingAssignmentProcessor; + } + + /** + * Resets quote shipping assignments when item is removed from multishipping quote. + * + * @param Quote $subject + * @param mixed $itemId + * + * @return array + */ + public function beforeRemoveItem(Quote $subject, $itemId): array + { + if ($subject->getIsMultiShipping()) { + $extensionAttributes = $subject->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $shippingAssignment = $this->shippingAssignmentProcessor->create($subject); + $extensionAttributes->setShippingAssignments([$shippingAssignment]); + } + } + + return [$itemId]; + } +} diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml new file mode 100644 index 0000000000000..67ba256f50ea7 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AdminSalesOrderActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <actionGroup name="AdminSalesOrderActionGroup"> + <waitForPageLoad stepKey="waitForAdminSalesPageToLoad"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRowLink"/> + <waitForPageLoad stepKey="waitForOrderPageToLoad"/> + <waitForPageLoad stepKey="waitForCheckTotalActionGroup"/> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <grabTextFrom selector="{{AdminOrderTotalSection.subTotal}}" stepKey="grabvalueForSubtotal"/> + <grabTextFrom selector="{{AdminOrderTotalSection.shippingAndHandling}}" stepKey="grabvalueForShippingHandling"/> + <grabTextFrom selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="grabvalueForGrandTotal"/> + <executeJS stepKey="sum_TotalValue" function=" + var subtotal = '{$grabvalueForSubtotal}'.substr(1); + var handling = '{$grabvalueForShippingHandling}'.substr(1); + var subtotal_handling = (parseFloat(subtotal) + parseFloat(handling)).toFixed(2); + return ('$' + subtotal_handling);"/> + <assertEquals stepKey="assertSubTotalPrice"> + <expectedResult type="string">$sum_TotalValue</expectedResult> + <actualResult type="string">$grabvalueForGrandTotal</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml index 333c2aec6c28e..861b97427b44d 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/CheckingWithMultipleAddressesActionGroup.xml @@ -23,5 +23,22 @@ <click stepKey="clickOnUpdateAddress" selector="{{SingleShippingSection.updateAddress}}" after="selectSecondShippingMethod" /> <waitForPageLoad stepKey="waitForShippingInformation" after="clickOnUpdateAddress" /> </actionGroup> + <actionGroup name="StorefrontCheckoutWithMultipleAddressesActionGroup"> + <click selector="{{SingleShippingSection.checkoutWithMultipleAddresses}}" stepKey="clickOnCheckoutWithMultipleAddresses"/> + <waitForPageLoad stepKey="waitForMultipleAddressPageLoad"/> + </actionGroup> + <actionGroup name="StorefrontSelectAddressActionGroup"> + <arguments> + <argument name="sequenceNumber" type="string" defaultValue="1"/> + <argument name="option" type="string" defaultValue="1"/> + </arguments> + <selectOption selector="{{MultishippingSection.selectShippingAddress(sequenceNumber)}}" userInput="{{option}}" stepKey="selectShippingAddress"/> + </actionGroup> + <actionGroup name="StorefrontSaveAddressActionGroup"> + <click stepKey="clickOnUpdateAddress" selector="{{SingleShippingSection.updateAddress}}"/> + <waitForPageLoad stepKey="waitForShippingInformationAfterUpdated" time="90"/> + <click stepKey="goToShippingInformation" selector="{{SingleShippingSection.goToShippingInfo}}"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml index efb860e314780..349d31ef1da5e 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/PlaceOrderActionGroup.xml @@ -16,5 +16,4 @@ <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> </actionGroup> -</actionGroups> - +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml index af7d897910ca3..bbd0e9ebad7aa 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/ReviewOrderActionGroup.xml @@ -35,5 +35,4 @@ </assertEquals> </actionGroup> -</actionGroups> - +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SalesOrderActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SalesOrderActionGroup.xml new file mode 100644 index 0000000000000..47cc3ffa455a0 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SalesOrderActionGroup.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <actionGroup name="SalesOrderForMultiShipmentActionGroup"> + <arguments> + <argument name="shippingPrice" defaultValue="$5.00" type="string" /> + <argument name="subtotalPrice" defaultValue="$123.00" type="string" /> + <argument name="totalPrice" defaultValue="$128.00" type="string" /> + </arguments> + <waitForPageLoad stepKey="waitForSalesOrderHistoryPageToLoad" /> + <!--Click on View Order Link--> + <click stepKey="viewOrderAction" selector="{{SalesOrderSection.viewOrderLink}}"/> + <waitForPageLoad stepKey="waitForViewOrderPageToLoad" /> + <!--Check Shipping Method, Subtotal and Total Price--> + <grabTextFrom selector="{{SalesOrderSection.salesOrderPrice('subtotal')}}" stepKey="salesOrderSubtotalPrice"/> + <grabTextFrom selector="{{SalesOrderSection.salesOrderPrice('shipping')}}" stepKey="salesOrderShippingPrice"/> + <grabTextFrom selector="{{SalesOrderSection.salesOrderPrice('grand_total')}}" stepKey="salesOrderGrandTotalPrice"/> + <assertEquals stepKey="assertSubtotalPrice"> + <expectedResult type="string">{{subtotalPrice}}</expectedResult> + <actualResult type="string">$salesOrderSubtotalPrice</actualResult> + </assertEquals> + <assertEquals stepKey="assertShippingMethodPrice"> + <expectedResult type="string">{{shippingPrice}}</expectedResult> + <actualResult type="string">$salesOrderShippingPrice</actualResult> + </assertEquals> + <assertEquals stepKey="assertTotalPrice"> + <expectedResult type="string">{{totalPrice}}</expectedResult> + <actualResult type="string">$salesOrderGrandTotalPrice</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml index 3f7578953df70..c5dd97cadcc2d 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectBillingInfoActionGroup.xml @@ -12,5 +12,4 @@ <waitForPageLoad stepKey="waitForBillingInfoPageLoad"/> <click stepKey="goToReviewOrder" selector="{{PaymentMethodSection.goToReviewOrder}}"/> </actionGroup> -</actionGroups> - +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml index af0b2467862ba..bcaeb8ba4800c 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/SelectShippingInfoActionGroup.xml @@ -29,5 +29,9 @@ <waitForPageLoad stepKey="waitForRadioOptions"/> <click stepKey="goToBillingInformation" selector="{{ShippingMethodSection.goToBillingInfo}}"/> </actionGroup> + <actionGroup name="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup"> + <waitForPageLoad stepKey="waitForShippingInfo"/> + <click stepKey="goToBillingInformation" selector="{{ShippingMethodSection.goToBillingInfo}}"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml new file mode 100644 index 0000000000000..c5dee010239d7 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontMultishippingCheckoutActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <actionGroup name="StorefrontCheckoutShippingSelectMultipleAddressesActionGroup"> + <arguments> + <argument name="firstAddress" type="string" defaultValue="{{CustomerAddressSimple.street[0]}}"/> + <argument name="secondAddress" type="string" defaultValue="{{CustomerAddressSimple.street[1]}}"/> + </arguments> + <selectOption selector="{{StorefrontCheckoutShippingMultipleAddressesSection.selectedMultipleShippingAddress('1')}}" userInput="{{firstAddress}}" stepKey="selectShippingAddressForTheFirstItem"/> + <selectOption selector="{{StorefrontCheckoutShippingMultipleAddressesSection.selectedMultipleShippingAddress('2')}}" userInput="{{secondAddress}}" stepKey="selectShippingAddressForTheSecondItem"/> + <click selector="{{CheckoutSuccessMainSection.continueShoppingButton}}" stepKey="clickToGoToInformationButton"/> + </actionGroup> + <actionGroup name="StorefrontGoCheckoutWithMultipleAddresses"> + <click selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="clickToMultipleAddressShippingButton"/> + </actionGroup> + <actionGroup name="StorefrontGoToBillingInformationActionGroup"> + <click selector="{{StorefrontMultipleShippingMethodSection.continueToBillingInformationButton}}" stepKey="clickToContinueToBillingInformationButton"/> + <waitForPageLoad stepKey="waitForBillingPage"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml index 001002e98271c..beee76a632b22 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/MultishippingCheckoutPage.xml @@ -15,4 +15,4 @@ <section name="PaymentMethodSection"/> <section name="ReviewOrderSection"/> </page> -</pages> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml index 45fafc3105c38..e6f3282493718 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml @@ -16,9 +16,17 @@ </section> <section name="MultishippingSection"> <element name="checkoutWithMultipleAddresses" type="button" selector="//span[text()='Check Out with Multiple Addresses']"/> + <element name="shippingMultipleCheckout" type="button" selector=".action.multicheckout"/> <element name="firstShippingAddressValue" type="select" selector="//table//tbody//tr[position()=1]//td[position()=3]//div//select//option[2]"/> <element name="firstShippingAddressOption" type="select" selector="//table//tbody//tr[position()=1]//td[position()=3]//div//select"/> <element name="secondShippingAddressValue" type="select" selector="//table//tbody//tr[position()=2]//td[position()=3]//div//select//option[1]"/> <element name="secondShippingAddressOption" type="select" selector="//table//tbody//tr[position()=2]//td[position()=3]//div//select"/> + <element name="selectShippingAddress" type="select" selector="(//table[@id='multiship-addresses-table'] //div[@class='field address'] //select)[{{sequenceNumber}}]" parameterized="true"/> + </section> + <section name="StorefrontMultipleShippingMethodSection"> + <element name="orderId" type="text" selector=".shipping-list:nth-child({{rowNum}}) .order-id" parameterized="true"/> + <element name="goToReviewYourOrderButton" type="button" selector="#payment-continue"/> + <element name="continueToBillingInformationButton" type="button" selector=".action.primary.continue"/> + <element name="successMessage" type="text" selector=".multicheckout.success"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml index 4e7f4a497ad4d..8113ed3aa0c07 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/PaymentMethodSection.xml @@ -11,4 +11,4 @@ <section name="PaymentMethodSection"> <element name="goToReviewOrder" type="button" selector="//span[text()='Go to Review Your Order']"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml index e13f28929dcc8..7961a0f811f64 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ReviewOrderSection.xml @@ -23,5 +23,4 @@ <element name="secondOrderTotalPrice" type="text" selector="//div[@class='block-content'][position()=2]//table[position()=1]//tr[@class='grand totals'][position()=1]//td//span[@class='price']"/> <element name="grandTotalPrice" type="text" selector="//div[@class='checkout-review']//div[@class='grand totals']//span[@class='price']"/> </section> -</sections> - +</sections> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/SalesOrderSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/SalesOrderSection.xml new file mode 100644 index 0000000000000..c788ef5978ad5 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/SalesOrderSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <section name="SalesOrderSection"> + <element name="viewOrderLink" type="text" selector="//span[text()='View Order']"/> + <element name="salesOrderPrice" type="text" selector="//div[@class='order-details-items ordered']//tr[@class='{{price_type}}']//td[@class='amount']//span[@class='price']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml index 6a2290bcf1a43..311b3ae959069 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml @@ -14,5 +14,4 @@ <element name="secondShippingMethodRadioButton" type="select" selector="//div[@class='block block-shipping'][position()=2]//div[@class='block-content']//div[@class='box box-shipping-method']//div[@class='box-content']//dl//dd[position()=2]//fieldset//div//div//input[@class='radio']"/> <element name="goToBillingInfo" type="button" selector="//span[text()='Continue to Billing Information']"/> </section> -</sections> - +</sections> \ No newline at end of file diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml new file mode 100644 index 0000000000000..34427bda9334a --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontCheckoutShippingMultipleAddressesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutShippingMultipleAddressesSection"> + <element name="selectedMultipleShippingAddress" type="select" selector=".table tr:nth-of-type({{selectNumber}}) select" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml new file mode 100644 index 0000000000000..a81d24e99563a --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMyAccountWithMultishipmentTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontMyAccountWithMultishipmentTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Shipping price shows 0 on Order view page after multiple address checkout"/> + <title value="Verify Shipping price for Storefront after multiple address checkout"/> + <description value="Verify that shipping price on My account matches with shipping method prices after multiple addresses checkout (Order view page)"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-19303"/> + <group value="multishipping"/> + </annotations> + + <before> + <createData stepKey="category" entity="SimpleSubCategory"/> + <createData stepKey="product1" entity="SimpleProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <createData stepKey="product2" entity="SimpleProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <createData entity="FlatRateShippingMethodDefault" stepKey="enableFlatRateShipping"/> + <magentoCLI command="config:set payment/checkmo/active 1" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </before> + + <amOnPage url="$$product1.name$$.html" stepKey="goToProduct1"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProduct1"> + <argument name="productName" value="$$product1.name$$"/> + </actionGroup> + <amOnPage url="$$product2.name$$.html" stepKey="goToProduct2"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProduct2"> + <argument name="productName" value="$$product2.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethod"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"/> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToSalesOrder"/> + <actionGroup ref="SalesOrderForMultiShipmentActionGroup" stepKey="salesOrderForMultiShipment"/> + <waitForPageLoad stepKey="waitForAdminPageToLoad"/> + <!-- Go to Stores > Configuration > Sales > Orders --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onAdminOrdersPage"/> + <actionGroup ref="AdminSalesOrderActionGroup" stepKey="ValidateOrderTotals"/> + <after> + <deleteData stepKey="deleteCategory" createDataKey="category"/> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml new file mode 100644 index 0000000000000..138ab5df26ab0 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckoutWithMultipleAddressesTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Multiple Shipping"/> + <title value="Place an order with three different addresses"/> + <description value="Place an order with three different addresses"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17461"/> + <useCaseId value="MAGETWO-99490"/> + <group value="Multishipment"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Set configurations --> + <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> + <!-- Create simple products --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="secondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Customer_US_UK_DE" stepKey="createCustomerWithMultipleAddresses"/> + </before> + <after> + <!-- Delete created data --> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + </after> + <!-- Login to the Storefront as created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomerWithMultipleAddresses$$"/> + </actionGroup> + <!-- Open the first product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToFirstProductPage"> + <argument name="productUrl" value="$$firstProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the first product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addFirstProductToCart"> + <argument name="productName" value="$$firstProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!-- Open the second product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToSecondProductPage"> + <argument name="productUrl" value="$$secondProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the second product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addSecondProductToCart"> + <argument name="productName" value="$$secondProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!--Go to Cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <!--Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSecondAddress"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <!-- Click 'Continue to Billing Information' --> + <actionGroup ref="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup" stepKey="useDefaultShippingMethod"/> + <!-- Click 'Go to Review Your Order' --> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="useDefaultBillingMethod"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <!-- Open the first product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToFirstProductPageSecondTime"> + <argument name="productUrl" value="$$firstProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add three identical products to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPage" stepKey="addIdenticalProductsToCart"> + <argument name="productName" value="$$firstProduct.name$$"/> + <argument name="productQty" value="3"/> + </actionGroup> + <!--Go to Cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCartWithIdenticalProducts"/> + <!--Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithThreeDifferentAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddressFromThree"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSecondAddressFromThree"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectThirdAddressFromThree"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveThreeDifferentAddresses"/> + <!-- Click 'Continue to Billing Information' --> + <actionGroup ref="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup" stepKey="useDefaultShippingMethodForIdenticalProducts"/> + <!-- Click 'Go to Review Your Order' --> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="UseDefaultBillingMethodForIdenticalProducts"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrderWithIdenticalProducts"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml new file mode 100644 index 0000000000000..fd79d4d954cd4 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Multishipping"/> + <title value="Process multishipping checkout when Cart page is opened in another tab"/> + <description value="Process multishipping checkout when Cart page is opened in another tab"/> + <severity value="MAJOR"/> + <testCaseId value="MC-17871"/> + <useCaseId value="MC-17469"/> + <group value="multishipping"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Set configurations --> + <magentoCLI command="config:set multishipping/options/checkout_multiple 1" stepKey="allowShippingToMultipleAddresses"/> + <!-- Create two simple products --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomerWithMultipleAddresses"/> + </before> + <after> + <!-- Delete created data --> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + </after> + <!-- Login to the Storefront as created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomerWithMultipleAddresses$$"/> + </actionGroup> + <!-- Add two products to the Shopping Cart --> + <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.name$$)}}" stepKey="amOnStorefrontProductFirstPage"/> + <waitForPageLoad stepKey="waitForTheFirstProduct"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddProductToCart"> + <argument name="product" value="$$createFirstProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.name$$)}}" stepKey="amOnStorefrontSecondProductPage"/> + <waitForPageLoad stepKey="waitForPageLoadForTheSecondProduct"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSecondProductToCart"> + <argument name="product" value="$$createSecondProduct$$"/> + <argument name="productCount" value="2"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnShoppingCartPage"/> + <!-- Click 'Check Out with Multiple Addresses' --> + <waitForPageLoad stepKey="waitForSecondPageLoad"/> + <actionGroup ref="StorefrontGoCheckoutWithMultipleAddresses" stepKey="goCheckoutWithMultipleAddresses"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontCheckoutShippingSelectMultipleAddressesActionGroup" stepKey="selectMultipleAddresses"> + <argument name="firstAddress" value="{{UK_Not_Default_Address.street[0]}}"/> + <argument name="secondAddress" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitPageLoad"/> + <!-- Open the Cart page in another browser window and go back --> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnShoppingCartPageNewTab"/> + <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertFirstProductItemInCheckOutCart"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + <argument name="productSku" value="$$createFirstProduct.sku$$"/> + <argument name="productPrice" value="$$createFirstProduct.price$$"/> + <argument name="subtotal" value="$$createFirstProduct.price$$" /> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSecondProductItemInCheckOutCart"> + <argument name="productName" value="$$createSecondProduct.name$$"/> + <argument name="productSku" value="$$createSecondProduct.sku$$"/> + <argument name="productPrice" value="$$createSecondProduct.price$$"/> + <argument name="subtotal" value="$$createSecondProduct.price$$" /> + <argument name="qty" value="1"/> + </actionGroup> + <switchToNextTab stepKey="switchToNextTab"/> + <!-- Click 'Continue to Billing Information' and 'Go to Review Your Order' --> + <actionGroup ref="StorefrontGoToBillingInformationActionGroup" stepKey="goToBillingInformation"/> + <see selector="{{ShipmentFormSection.shippingAddress}}" userInput="{{US_Address_NY.city}}" stepKey="seeBillingAddress"/> + <waitForElementVisible selector="{{StorefrontMultipleShippingMethodSection.goToReviewYourOrderButton}}" stepKey="waitForGoToReviewYourOrderVisible" /> + <click selector="{{StorefrontMultipleShippingMethodSection.goToReviewYourOrderButton}}" stepKey="clickToGoToReviewYourOrderButton"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <see selector="{{StorefrontMultipleShippingMethodSection.successMessage}}" userInput="Successfully ordered" stepKey="seeSuccessMessage"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <!-- Go to My Account > My Orders --> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <!-- Go to Admin > Sales > Orders --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchFirstOrder"> + <argument name="keyword" value="$grabFirstOrderId"/> + </actionGroup> + <seeElement selector="{{AdminOrdersGridSection.orderId({$grabFirstOrderId})}}" stepKey="seeAdminFirstOrder"/> + <actionGroup ref="searchAdminDataGridByKeyword" stepKey="searchSecondOrder"> + <argument name="keyword" value="$grabSecondOrderId"/> + </actionGroup> + <seeElement selector="{{AdminOrdersGridSection.orderId({$grabSecondOrderId})}}" stepKey="seeAdminSecondOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php index 9ffef2832a6bc..42715d026e9ed 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/Address/NewShippingTest.php @@ -170,7 +170,7 @@ public function executeDataProvider() { return [ 'shipping_address_exists' => ['*/checkout/addresses', 'shipping_address', 'back/address'], - 'shipping_address_not_exist' => ['*/cart/', null, 'back/cart'] + 'shipping_address_not_exist' => ['checkout/cart/', null, 'back/cart'] ]; } diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php deleted file mode 100644 index a26f2661ebab1..0000000000000 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Multishipping\Test\Unit\Controller\Checkout; - -use Magento\Multishipping\Controller\Checkout\Plugin; - -class PluginTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $cartMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteMock; - - /** - * @var Plugin - */ - protected $object; - - protected function setUp() - { - $this->cartMock = $this->createMock(\Magento\Checkout\Model\Cart::class); - $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping'] - ); - $this->cartMock->expects($this->once())->method('getQuote')->will($this->returnValue($this->quoteMock)); - $this->object = new \Magento\Multishipping\Controller\Checkout\Plugin($this->cartMock); - } - - public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(1); - $this->quoteMock->expects($this->once())->method('setIsMultiShipping')->with(0); - $this->cartMock->expects($this->once())->method('saveQuote'); - $this->object->beforeExecute($subject); - } - - public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); - $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); - $this->cartMock->expects($this->never())->method('saveQuote'); - $this->object->beforeExecute($subject); - } -} diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php index 02bc966873774..731365974c235 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php @@ -11,6 +11,7 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\AddressSearchResultsInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Directory\Model\Currency; use Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderDefault; use Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderFactory; use Magento\Quote\Model\Quote\Address; @@ -44,6 +45,7 @@ use Magento\Quote\Model\ShippingAssignment; use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit_Framework_MockObject_MockObject; use \PHPUnit\Framework\TestCase; @@ -453,7 +455,8 @@ public function testCreateOrders(): void ]; $quoteItemId = 1; $paymentProviderCode = 'checkmo'; - + $shippingPrice = '0.00'; + $currencyCode = 'USD'; $simpleProductTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\Simple::class) ->disableOriginalConstructor() ->setMethods(['getOrderOptions']) @@ -467,6 +470,17 @@ public function testCreateOrders(): void $this->getQuoteAddressesMock($quoteAddressItemMock, $addressTotal); $this->setQuoteMockData($paymentProviderCode, $shippingAddressMock, $billingAddressMock); + $currencyMock = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->setMethods([ 'convert' ]) + ->getMock(); + $currencyMock->method('convert')->willReturn($shippingPrice); + $storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->setMethods(['getBaseCurrency','getCurrentCurrencyCode' ]) + ->getMock(); + $storeMock->method('getBaseCurrency')->willReturn($currencyMock); + $storeMock->method('getCurrentCurrencyCode')->willReturn($currencyCode); $orderAddressMock = $this->createSimpleMock(\Magento\Sales\Api\Data\OrderAddressInterface::class); $orderPaymentMock = $this->createSimpleMock(\Magento\Sales\Api\Data\OrderPaymentInterface::class); $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) @@ -476,6 +490,9 @@ public function testCreateOrders(): void $orderItemMock->method('getQuoteItemId')->willReturn($quoteItemId); $orderMock = $this->getOrderMock($orderAddressMock, $orderPaymentMock, $orderItemMock); + $orderMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $orderMock->expects($this->once())->method('setBaseShippingAmount')->with($shippingPrice)->willReturnSelf(); + $orderMock->expects($this->once())->method('setShippingAmount')->with($shippingPrice)->willReturnSelf(); $this->orderFactoryMock->expects($this->once())->method('create')->willReturn($orderMock); $this->dataObjectHelperMock->expects($this->once())->method('mergeDataObjects') ->with( @@ -608,12 +625,18 @@ private function getQuoteAddressesMock($quoteAddressItemMock, int $addressTotal) )->getMock(); $shippingAddressMock->method('validate')->willReturn(true); $shippingAddressMock->method('getShippingMethod')->willReturn('carrier'); - $shippingAddressMock->method('getShippingRateByCode')->willReturn('code'); $shippingAddressMock->method('getCountryId')->willReturn('EN'); $shippingAddressMock->method('getAllItems')->willReturn([$quoteAddressItemMock]); $shippingAddressMock->method('getAddressType')->willReturn('shipping'); $shippingAddressMock->method('getGrandTotal')->willReturn($addressTotal); + $shippingRateMock = $this->getMockBuilder(Address\Rate::class) + ->disableOriginalConstructor() + ->setMethods([ 'getPrice' ]) + ->getMock(); + $shippingRateMock->method('getPrice')->willReturn('0.00'); + $shippingAddressMock->method('getShippingRateByCode')->willReturn($shippingRateMock); + $billingAddressMock = $this->getMockBuilder(Address::class) ->disableOriginalConstructor() ->setMethods(['validate']) @@ -673,6 +696,9 @@ private function getOrderMock( 'getCanSendNewEmailFlag', 'getItems', 'setShippingMethod', + 'getStore', + 'setShippingAmount', + 'setBaseShippingAmount' ] )->getMock(); $orderMock->method('setQuote')->with($this->quoteMock); diff --git a/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php new file mode 100644 index 0000000000000..02ae1a70ce801 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php @@ -0,0 +1,99 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Test\Unit\Plugin; + +use Magento\Checkout\Controller\Index\Index; +use Magento\Checkout\Model\Cart; +use Magento\Multishipping\Plugin\DisableMultishippingMode; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; + +/** + * Class DisableMultishippingModeTest + */ +class DisableMultishippingModeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $cartMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var DisableMultishippingMode + */ + private $object; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->cartMock = $this->createMock(Cart::class); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping', 'getExtensionAttributes'] + ); + $this->cartMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($this->quoteMock)); + $this->object = new DisableMultishippingMode($this->cartMock); + } + + /** + * Tests turn off multishipping on multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $extensionAttributes = $this->createPartialMock( + CartExtensionInterface::class, + ['setShippingAssignments', 'getShippingAssignments'] + ); + $extensionAttributes->method('getShippingAssignments') + ->willReturn( + $this->createMock(ShippingAssignmentInterface::class) + ); + $extensionAttributes->expects($this->once()) + ->method('setShippingAssignments') + ->with([]); + $this->quoteMock->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + $this->quoteMock->expects($this->once()) + ->method('getIsMultiShipping')->willReturn(1); + $this->quoteMock->expects($this->once()) + ->method('setIsMultiShipping') + ->with(0); + $this->cartMock->expects($this->once()) + ->method('saveQuote'); + + $this->object->beforeExecute($subject); + } + + /** + * Tests turn off multishipping on non-multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); + $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); + $this->cartMock->expects($this->never())->method('saveQuote'); + $this->object->beforeExecute($subject); + } +} diff --git a/app/code/Magento/Multishipping/etc/di.xml b/app/code/Magento/Multishipping/etc/di.xml index 3bccf0b74bcd8..ad0d341d6b2e9 100644 --- a/app/code/Magento/Multishipping/etc/di.xml +++ b/app/code/Magento/Multishipping/etc/di.xml @@ -9,4 +9,7 @@ <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="multishipping_shipping_addresses" type="Magento\Multishipping\Model\Cart\CartTotalRepositoryPlugin" /> </type> + <type name="Magento\Quote\Model\QuoteRepository"> + <plugin name="multishipping_quote_repository" type="Magento\Multishipping\Plugin\MultishippingQuoteRepository" /> + </type> </config> diff --git a/app/code/Magento/Multishipping/etc/frontend/di.xml b/app/code/Magento/Multishipping/etc/frontend/di.xml index 0c2daaf45043e..481b95280a4a4 100644 --- a/app/code/Magento/Multishipping/etc/frontend/di.xml +++ b/app/code/Magento/Multishipping/etc/frontend/di.xml @@ -31,13 +31,13 @@ </arguments> </type> <type name="Magento\Checkout\Controller\Cart\Add"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Cart\UpdatePost"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Index\Index"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Model\Cart"> <plugin name="multishipping_session_mapper" type="Magento\Multishipping\Model\Checkout\Type\Multishipping\Plugin" sortOrder="50" /> @@ -45,4 +45,7 @@ <type name="Magento\Checkout\Controller\Cart"> <plugin name="multishipping_clear_addresses" type="Magento\Multishipping\Model\Cart\Controller\CartPlugin" sortOrder="50" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="multishipping_reset_shipping_assigment" type="Magento\Multishipping\Plugin\ResetShippingAssigment"/> + </type> </config> diff --git a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php index bacdd3e4a81fe..99d3f4d406adc 100644 --- a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php +++ b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php @@ -7,6 +7,9 @@ use \Magento\Framework\HTTP\ZendClient; +/** + * Performs the request to make the deployment + */ class Deployments { /** @@ -88,7 +91,7 @@ public function setDeployment($description, $change = false, $user = false) return false; } - if (($response->getStatus() < 200 || $response->getStatus() > 210)) { + if ($response->getStatus() < 200 || $response->getStatus() > 210) { $this->logger->warning('Deployment marker request did not send a 200 status code.'); return false; } diff --git a/app/code/Magento/NewRelicReporting/etc/db_schema.xml b/app/code/Magento/NewRelicReporting/etc/db_schema.xml index c6e61b88f4b1b..e18d7c8077bb9 100644 --- a/app/code/Magento/NewRelicReporting/etc/db_schema.xml +++ b/app/code/Magento/NewRelicReporting/etc/db_schema.xml @@ -22,7 +22,7 @@ </table> <table name="reporting_module_status" resource="default" engine="innodb" comment="Module Status Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Module Id"/> + comment="Module ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Module Name"/> <column xsi:type="varchar" name="active" nullable="true" length="255" comment="Module Active Status"/> <column xsi:type="varchar" name="setup_version" nullable="true" length="255" comment="Module Version"/> diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index f9e9d57bf4b40..8ca489d89c1df 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -130,7 +130,7 @@ public function loadByEmail($subscriberEmail) */ public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer) { - $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds(); + $storeIds = $this->storeManager->getWebsite()->getStoreIds(); if ($customer->getId()) { $select = $this->connection diff --git a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml index 1df1cd5f8dae8..02657340bf34d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml @@ -19,8 +19,8 @@ <data key="dataUiId">magento-newsletter-newsletter-subscriber</data> </entity> <entity name="AdminMenuMarketingCommunicationsNewsletterTemplate"> - <data key="pageTitle">Newsletter Template</data> - <data key="title">Newsletter Template</data> + <data key="pageTitle">Newsletter Templates</data> + <data key="title">Newsletter Templates</data> <data key="dataUiId">magento-newsletter-newsletter-template</data> </entity> <entity name="AdminMenuReportsMarketingNewsletterProblemReports"> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index a7ac9e38d4b07..273a39a312132 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -15,10 +15,7 @@ <title value="Admin should be able to add widget to WYSIWYG Editor of Newsletter"/> <description value="Admin should be able to add widget to WYSIWYG Editor Newsletter"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-84682"/> - <skip> - <issueId value="MC-17140"/> - </skip> + <testCaseId value="MC-6070"/> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="login"/> @@ -26,13 +23,14 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForElementVisible selector="{{BasicFieldNewsletterSection.templateName}}" stepKey="waitForTemplateName"/> <fillField selector="{{BasicFieldNewsletterSection.templateName}}" userInput="{{_defaultNewsletter.name}}" stepKey="fillTemplateName" /> <fillField selector="{{BasicFieldNewsletterSection.templateSubject}}" userInput="{{_defaultNewsletter.subject}}" stepKey="fillTemplateSubject" /> <fillField selector="{{BasicFieldNewsletterSection.senderName}}" userInput="{{_defaultNewsletter.senderName}}" stepKey="fillSenderName" /> <fillField selector="{{BasicFieldNewsletterSection.senderEmail}}" userInput="{{_defaultNewsletter.senderEmail}}" stepKey="fillSenderEmail" /> <conditionalClick selector="{{NewsletterWYSIWYGSection.ShowHideBtn}}" dependentSelector="{{NewsletterWYSIWYGSection.TinyMCE4}}" visible="false" stepKey="toggleEditorIfHidden"/> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> + <waitForElementVisible selector="{{NewsletterWYSIWYGSection.InsertWidgetIcon}}" stepKey="waitForInsertWidgerIconButton"/> <click selector="{{NewsletterWYSIWYGSection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> <wait time="10" stepKey="waitForPageLoad" /> <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index 22ca214c94aec..4d60b7676605e 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -49,7 +49,7 @@ </actionGroup> <magentoCLI command="indexer:reindex" stepKey="reindex"/> <magentoCLI command="cache:flush" stepKey="flushCache"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Go to store front (default) and click Create an Account.--> diff --git a/app/code/Magento/Newsletter/etc/adminhtml/menu.xml b/app/code/Magento/Newsletter/etc/adminhtml/menu.xml index a9cedf1c7a1ee..8fc21494b5de7 100644 --- a/app/code/Magento/Newsletter/etc/adminhtml/menu.xml +++ b/app/code/Magento/Newsletter/etc/adminhtml/menu.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_Newsletter::newsletter_template" title="Newsletter Template" translate="title" module="Magento_Newsletter" parent="Magento_Backend::marketing_communications" sortOrder="30" action="newsletter/template/" resource="Magento_Newsletter::template"/> + <add id="Magento_Newsletter::newsletter_template" title="Newsletter Templates" translate="title" module="Magento_Newsletter" parent="Magento_Backend::marketing_communications" sortOrder="30" action="newsletter/template/" resource="Magento_Newsletter::template"/> <add id="Magento_Newsletter::newsletter_queue" title="Newsletter Queue" translate="title" module="Magento_Newsletter" sortOrder="40" parent="Magento_Backend::marketing_communications" action="newsletter/queue/" resource="Magento_Newsletter::queue"/> <add id="Magento_Newsletter::newsletter_subscriber" title="Newsletter Subscribers" translate="title" module="Magento_Newsletter" sortOrder="50" parent="Magento_Backend::marketing_communications" action="newsletter/subscriber/" resource="Magento_Newsletter::subscriber"/> <add id="Magento_Newsletter::newsletter_problem" title="Newsletter Problem Reports" translate="title" module="Magento_Newsletter" sortOrder="50" parent="Magento_Reports::report_marketing" action="newsletter/problem/" resource="Magento_Newsletter::problem"/> diff --git a/app/code/Magento/Newsletter/etc/db_schema.xml b/app/code/Magento/Newsletter/etc/db_schema.xml index 5cb572f41b6be..257416d0bc465 100644 --- a/app/code/Magento/Newsletter/etc/db_schema.xml +++ b/app/code/Magento/Newsletter/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="newsletter_subscriber" resource="default" engine="innodb" comment="Newsletter Subscriber"> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Subscriber Id"/> + comment="Subscriber ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="change_status_at" on_update="false" nullable="true" comment="Change Status At"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Id"/> + default="0" comment="Customer ID"/> <column xsi:type="varchar" name="subscriber_email" nullable="true" length="150" comment="Subscriber Email"/> <column xsi:type="int" name="subscriber_status" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Subscriber Status"/> @@ -69,7 +69,7 @@ </table> <table name="newsletter_queue" resource="default" engine="innodb" comment="Newsletter Queue"> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Queue Id"/> + comment="Queue ID"/> <column xsi:type="int" name="template_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Template ID"/> <column xsi:type="int" name="newsletter_type" padding="11" unsigned="false" nullable="true" identity="false" @@ -98,11 +98,11 @@ </table> <table name="newsletter_queue_link" resource="default" engine="innodb" comment="Newsletter Queue Link"> <column xsi:type="int" name="queue_link_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Queue Link Id"/> + comment="Queue Link ID"/> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Subscriber Id"/> + default="0" comment="Subscriber ID"/> <column xsi:type="timestamp" name="letter_sent_at" on_update="false" nullable="true" comment="Letter Sent At"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_link_id"/> @@ -123,9 +123,9 @@ </table> <table name="newsletter_queue_store_link" resource="default" engine="innodb" comment="Newsletter Queue Store Link"> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_id"/> <column name="store_id"/> @@ -142,11 +142,11 @@ </table> <table name="newsletter_problem" resource="default" engine="innodb" comment="Newsletter Problems"> <column xsi:type="int" name="problem_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Problem Id"/> + comment="Problem ID"/> <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Subscriber Id"/> + comment="Subscriber ID"/> <column xsi:type="int" name="queue_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Queue Id"/> + default="0" comment="Queue ID"/> <column xsi:type="int" name="problem_error_code" padding="10" unsigned="true" nullable="true" identity="false" default="0" comment="Problem Error Code"/> <column xsi:type="varchar" name="problem_error_text" nullable="true" length="200" comment="Problem Error Text"/> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml index 36f9d35327fce..28395f8eeb849 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($block->getInfo()->getAdditionalInformation()) : ?> <?php if ($block->getPayableTo()) : ?> <br /><?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml index d8d952526e67b..f85a8f8357dd9 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> {{pdf_row_separator}} <?php if ($block->getInfo()->getAdditionalInformation()) : ?> {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml index 2a6de7f0cc356..ae7f654a1350b 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml @@ -6,8 +6,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<div class="order-payment-method-name"><?= $block->escapeHtml($block->getMethod()->getTitle()) ?></div> +<div class="order-payment-method-name"><?= $block->escapeHtml($paymentTitle) ?></div> <table class="data-table admin__table-secondary"> <tr> <th><?= $block->escapeHtml(__('Purchase Order Number')) ?>:</th> diff --git a/app/code/Magento/OfflineShipping/etc/db_schema.xml b/app/code/Magento/OfflineShipping/etc/db_schema.xml index 5129e8a29b2a1..6bda6597e2f61 100644 --- a/app/code/Magento/OfflineShipping/etc/db_schema.xml +++ b/app/code/Magento/OfflineShipping/etc/db_schema.xml @@ -11,11 +11,11 @@ <column xsi:type="int" name="pk" padding="10" unsigned="true" nullable="false" identity="true" comment="Primary key"/> <column xsi:type="int" name="website_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="varchar" name="dest_country_id" nullable="false" length="4" default="0" comment="Destination coutry ISO/2 or ISO/3 code"/> <column xsi:type="int" name="dest_region_id" padding="11" unsigned="false" nullable="false" identity="false" - default="0" comment="Destination Region Id"/> + default="0" comment="Destination Region ID"/> <column xsi:type="varchar" name="dest_zip" nullable="false" length="10" default="*" comment="Destination Post Code (Zip)"/> <column xsi:type="varchar" name="condition_name" nullable="false" length="30" comment="Rate Condition name"/> diff --git a/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php b/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php index 6cdc500aaf33c..36a20ec658b6f 100644 --- a/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php +++ b/app/code/Magento/PageCache/Plugin/RegisterFormKeyFromCookie.php @@ -10,10 +10,10 @@ namespace Magento\PageCache\Plugin; use Magento\Framework\App\PageCache\FormKey as CacheFormKey; -use Magento\Framework\Escaper; use Magento\Framework\Data\Form\FormKey; -use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Escaper; use Magento\Framework\Session\Config\ConfigInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; /** * Allow for registration of a form key through cookies. @@ -46,7 +46,7 @@ class RegisterFormKeyFromCookie private $sessionConfig; /** - * @param CacheFormKey $formKey + * @param CacheFormKey $cacheFormKey * @param Escaper $escaper * @param FormKey $formKey * @param CookieMetadataFactory $cookieMetadataFactory @@ -70,7 +70,6 @@ public function __construct( * Set form key from the cookie. * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeDispatch(): void @@ -85,6 +84,8 @@ public function beforeDispatch(): void } /** + * Set form key cookie + * * @param string $formKey * @return void */ @@ -94,6 +95,7 @@ private function updateCookieFormKey(string $formKey): void ->createPublicCookieMetadata(); $cookieMetadata->setDomain($this->sessionConfig->getCookieDomain()); $cookieMetadata->setPath($this->sessionConfig->getCookiePath()); + $cookieMetadata->setSecure($this->sessionConfig->getCookieSecure()); $lifetime = $this->sessionConfig->getCookieLifetime(); if ($lifetime !== 0) { $cookieMetadata->setDuration($lifetime); diff --git a/app/code/Magento/PageCache/etc/adminhtml/system.xml b/app/code/Magento/PageCache/etc/adminhtml/system.xml index 8013ad40ef5aa..234e3e48a95d8 100644 --- a/app/code/Magento/PageCache/etc/adminhtml/system.xml +++ b/app/code/Magento/PageCache/etc/adminhtml/system.xml @@ -74,6 +74,7 @@ </group> <field id="ttl" type="text" translate="label comment" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>TTL for public content</label> + <validate>validate-zero-or-greater validate-digits</validate> <comment>Public content cache lifetime in seconds. If field is empty default value 86400 will be saved. </comment> <backend_model>Magento\PageCache\Model\System\Config\Backend\Ttl</backend_model> </field> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml index 8b9c37f112560..3cd88bddbfb1f 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml @@ -9,8 +9,9 @@ * @see \Magento\Payment\Block\Info */ $specificInfo = $block->getSpecificInformation(); +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($specificInfo) : ?> <table class="data-table admin__table-secondary"> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml index a8583ea5549fe..54b9e48d07a94 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml @@ -8,8 +8,9 @@ * @see \Magento\Payment\Block\Info * @var \Magento\Payment\Block\Info $block */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} +<?= $block->escapeHtml($paymentTitle) ?>{{pdf_row_separator}} <?php if ($specificInfo = $block->getSpecificInformation()) : ?> <?php foreach ($specificInfo as $label => $value) : ?> diff --git a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js index c41be40cba144..746a4bd2cf33b 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js @@ -4,23 +4,15 @@ */ /* @api */ -(function (factory) { - 'use strict'; - - if (typeof define === 'function' && define.amd) { - define([ - 'jquery', - 'Magento_Payment/js/model/credit-card-validation/cvv-validator', - 'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator', - 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-year-validator', - 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-month-validator', - 'Magento_Payment/js/model/credit-card-validation/credit-card-data', - 'mage/translate' - ], factory); - } else { - factory(jQuery); - } -}(function ($, cvvValidator, creditCardNumberValidator, yearValidator, monthValidator, creditCardData) { +define([ + 'jquery', + 'Magento_Payment/js/model/credit-card-validation/cvv-validator', + 'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator', + 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-year-validator', + 'Magento_Payment/js/model/credit-card-validation/expiration-date-validator/expiration-month-validator', + 'Magento_Payment/js/model/credit-card-validation/credit-card-data', + 'mage/translate' +], function ($, cvvValidator, creditCardNumberValidator, yearValidator, monthValidator, creditCardData) { 'use strict'; $('.payment-method-content input[type="number"]').on('keyup', function () { @@ -111,4 +103,4 @@ rule.unshift(i); $.validator.addMethod.apply($.validator, rule); }); -})); +}); diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml index c047633e5227e..4d752d8377640 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml @@ -16,7 +16,31 @@ <argument name="credentials" defaultValue="_CREDS"/> <argument name="countryCode" type="string" defaultValue="us"/> </arguments> - + + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_business_account}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_merchant_id}}" stepKey="inputMerchantID"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + </actionGroup> + + <actionGroup name="SampleConfigPayPalExpressCheckout"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalExpressConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> @@ -29,42 +53,44 @@ <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> - <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> </actionGroup> - + <actionGroup name="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" extends="CreateOrderToPrintPageActionGroup"> <annotations> <description>EXTENDS: CreateOrderToPrintPageActionGroup. Clicks on PayPal. Fills the PayPay details in the modal. PLEASE NOTE: The PayPal Payment credentials are Hardcoded using 'Payer'.</description> </annotations> - + <arguments> + <argument name="payerName" defaultValue="MPI" type="string"/> + <argument name="credentials" defaultValue="_CREDS"/> + </arguments> + + <!-- click on PayPal payment radio button --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="clickPlaceOrder"/> - + <!--set ID for iframe of PayPal group button--> <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> - + <!--switch to iframe of PayPal group button--> - <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> <click selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="clickPayPalBtn"/> <switchToIFrame stepKey="switchBack1"/> - + <!--Check in-context--> - <comment userInput="Check in-context" stepKey="commentVerifyInContext"/> <switchToNextTab stepKey="switchToInContentTab"/> <waitForPageLoad stepKey="waitForPageLoad"/> <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> - <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm"/> - <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{Payer.buyerEmail}}" stepKey="fillEmail"/> - <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{Payer.buyerPassword}}" stepKey="fillPassword"/> + <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> - <seeElement selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> </actionGroup> - + <actionGroup name="addProductToCheckoutPage"> <annotations> <description>Goes to the provided Category page on the Storefront. Adds the 1st Product to the Cart. Goes to Checkout. Select the Shipping Method. Selects PayPal as the Payment Method.</description> @@ -72,7 +98,7 @@ <arguments> <argument name="Category"/> </arguments> - + <amOnPage url="{{StorefrontCategoryPage.url(Category.name)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml new file mode 100644 index 0000000000000..392014d876e46 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontPayOrderOnPayPalCheckoutActionGroup"> + <annotations> + <description>Verifies product name on Paypal cart and clicks 'Pay Now' on PayPal payment checkout page.</description> + </annotations> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> + <seeElement selector="{{PayPalPaymentSection.itemName(productName)}}" stepKey="seeProductName"/> + <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml new file mode 100644 index 0000000000000..0744207494108 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="StorefrontPaypalEnableConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalMerchantAccountIdConfigData"> + <data key="path">payment/paypal_express/merchant_id</data> + <data key="scope_id">1</data> + <data key="value">''</data> + </entity> + <entity name="StorefrontPaypalEnableSkipOrderReviewStepConfigData"> + <data key="path">payment/paypal_express/skip_order_review_step</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableSkipOrderReviewStepConfigData"> + <data key="path">payment/paypal_express/skip_order_review_step</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalEnableInContextCheckoutConfigData"> + <data key="path">payment/paypal_express/in_context</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableInContextCheckoutConfigData"> + <data key="path">payment/paypal_express/active</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index ae34476e9ac0b..ba56243fdb391 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -99,4 +99,37 @@ <data key="paypal_express_api_signature">someApiSignature</data> <data key="paypal_express_merchantID">someMerchantId</data> </entity> + <entity name="PaypalConfig" type="paypal_config_state"> + <requiredEntity type="business_account">BusinessAccount</requiredEntity> + <requiredEntity type="api_username">ApiUsername</requiredEntity> + <requiredEntity type="api_password">ApiPassword</requiredEntity> + <requiredEntity type="api_signature">ApiSignature</requiredEntity> + <requiredEntity type="api_authentication">ApiAuthentication</requiredEntity> + <requiredEntity type="sandbox_flag">SandboxFlag</requiredEntity> + <requiredEntity type="use_proxy">UseProxy</requiredEntity> + </entity> + <entity name="BusinessAccount" type="business_account"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_business_account}}</data> + </entity> + <entity name="ApiUsername" type="api_username"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_username}}</data> + </entity> + <entity name="ApiPassword" type="api_password"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_password}}</data> + </entity> + <entity name="ApiSignature" type="api_signature"> + <data key="value">{{_CREDS.magento/paypal_express_checkout_us_api_signature}}</data> + </entity> + <entity name="ApiAuthentication" type="api_authentication"> + <data key="value">0</data> + </entity> + <entity name="SandboxFlag" type="sandbox_flag"> + <data key="value">1</data> + </entity> + <entity name="UseProxy" type="use_proxy"> + <data key="value">0</data> + </entity> + <entity name="Payer"> + <data key="firstName">Alex</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml index 8d1b594d44e61..af68a7611cd1d 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -57,7 +57,7 @@ <element name="email" type="input" selector="//input[contains(@name, 'email') and not(contains(@style, 'display:none'))]"/> <element name="password" type="input" selector="//input[contains(@name, 'password') and not(contains(@style, 'display:none'))]"/> <element name="loginBtn" type="input" selector="button#btnLogin"/> - <element name="reviewUserInfo" type="text" selector="//p[@id='reviewUserInfo' and contains(text(),'Hi, MPI!')]"/> + <element name="reviewUserInfo" type="text" selector="#reviewUserInfo"/> <element name="cartIcon" type="text" selector="#transactionCart"/> <element name="itemName" type="text" selector="//span[@title='{{productName}}']" parameterized="true"/> <element name="PayPalSubmitBtn" type="text" selector="//input[@type='submit']"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml new file mode 100644 index 0000000000000..5c10bc9536fcf --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminCheckDefaultValueOfPayPalCustomizeButtonTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckDefaultValueOfPayPalCustomizeButtonTest"> + <annotations> + <features value="Paypal"/> + <stories value="Button Configuration"/> + <title value="Check Default Value Of Paypal Customize Button"/> + <description value="Default value of Paypal Customize Button should be NO"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10904"/> + <group value="paypal"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey= "openPayPalButtonCheckoutPage"/> + <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify default value--> + <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> + <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> + <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> + <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml index a6e741f0151e0..cfc7d66ba23cf 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml @@ -20,7 +20,7 @@ </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> + <actionGroup ref="SampleConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> <argument name="credentials" value="SamplePaypalExpressConfig"/> </actionGroup> </before> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml deleted file mode 100644 index 079b46dc1b0cb..0000000000000 --- a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml +++ /dev/null @@ -1,170 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CheckDefaultValueOfPayPalCustomizeButtonTest"> - <annotations> - <features value="PayPal"/> - <stories value="Button Configuration"/> - <title value="Check Default Value Of PayPal Customize Button"/> - <description value="Default value of PayPal Customize Button should be NO"/> - <severity value="AVERAGE"/> - <testCaseId value="MC-10904"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - </before> - <after> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> - <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> - <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> - <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> - <!--Verify default value--> - <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> - <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> - <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> - <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> - <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> - <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> - </test> - <test name="CheckCreditButtonConfiguration"> - <annotations> - <features value="PayPal"/> - <stories value="Button Configuration"/> - <title value="Check Credit Button Configuration"/> - <description value="Admin is able to customize Credit button"/> - <severity value="AVERAGE"/> - <testCaseId value="MC-10900"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - <createData entity="_defaultProduct" stepKey="createPreReqProduct"> - <requiredEntity createDataKey="createPreReqCategory"/> - </createData> - <!-- Create Customer --> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <!--Config PayPal Express Checkout--> - <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - </before> - <after> - <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> - <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> - <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <!--Navigate to button configuration setting--> - <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> - <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> - <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> - <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> - <!--Verify Credit Button value--> - <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> - <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> - <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize2"/> - <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape2"/> - <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> - <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> - <!--Customize Credit Button--> - <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> - <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> - <!--Save configuration--> - <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> - <waitForPageLoad stepKey="waitForConfigSave"/> - <openNewTab stepKey="openNewTab"/> - <amOnPage url="/" stepKey="openStorefront"/> - <!--Login to storefront as previously created customer--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> - <argument name="Category" value="$$createPreReqCategory$$"/> - </actionGroup> - <!--set ID for iframe of PayPal group button--> - <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> - <!--switch to iframe of PayPal group button--> - <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> - <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> - <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> - </test> - <test name="PayPalSmartButtonInCheckoutPage"> - <annotations> - <features value="PayPal"/> - <stories value="Generic checkout skeleton flow"/> - <title value="Mainflow of PayPal Smart Button"/> - <description value="Users are able to place order using PayPal Smart Button"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-13690"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - <createData entity="_defaultProduct" stepKey="createPreReqProduct"> - <requiredEntity createDataKey="createPreReqCategory"/> - </createData> - <!-- Create Customer --> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <actionGroup ref="LoginActionGroup" stepKey="login"/> - <!--Config PayPal Express Checkout--> - <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - <magentoCLI command="config:set payment/paypal_express/in_context 1" stepKey="disableInContextPayPal"/> - </before> - <after> - <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> - <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> - <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> - <magentoCLI command="config:set payment/paypal_express/in_context 0" stepKey="enableInContextPayPal"/> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> - </after> - <magentoCLI command="config:set payment/paypal_express/payment_action Authorization" stepKey="inputPaymentAction"/> - <magentoCLI command="config:set payment/paypal_express/solution_type Sole" stepKey="enablePayPalGuestCheckout"/> - <magentoCLI command="config:set payment/paypal_express/line_items_enabled 1" stepKey="enableTransferCartLine"/> - <magentoCLI command="config:set payment/paypal_express/skip_order_review_step 1" stepKey="enableSkipOrderReview"/> - <!--Login to storefront as previously created customer--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - <!--Place an order using PayPal method--> - <comment userInput="Place an order using PayPal method" stepKey="commentPayPalPlaceOrder"/> - <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> - <argument name="Category" value="$$createPreReqCategory$$"/> - </actionGroup> - <!--Open Cart on PayPal--> - <comment userInput="Open Cart on PayPal" stepKey="commentOpenCart"/> - <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> - <seeElement selector="{{PayPalPaymentSection.itemName($$createPreReqProduct.name$$)}}" stepKey="seeProductname"/> - <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> - <switchToPreviousTab stepKey="switchToPreviousTab"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <!--I see order successful Page instead of Order Review Page--> - <comment userInput="I see order successful Page instead of Order Review Page" stepKey="commentVerifyOrderReviewPage"/> - <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> - <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> - <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml new file mode 100644 index 0000000000000..d8cea82bcc2f5 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckCreditButtonConfigurationTest"> + <annotations> + <features value="Paypal"/> + <stories value="Button Configuration"/> + <title value="Check Credit Button Configuration"/> + <description value="Admin is able to customize Credit button"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10900"/> + <group value="paypal"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Navigate to button configuration setting--> + <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify Credit Button value--> + <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> + <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape"/> + <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> + <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> + <!--Customize Credit Button--> + <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> + <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSave"/> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="/" stepKey="openStorefront"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--set ID for iframe of PayPal group button--> + <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> + <!--switch to iframe of PayPal group button--> + <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> + <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml new file mode 100644 index 0000000000000..6adba94e96890 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonInCheckoutPageTest"> + <annotations> + <features value="Paypal"/> + <stories value="Generic checkout skeleton flow"/> + <title value="Mainflow of Paypal Smart Button"/> + <description value="Users are able to place order using Paypal Smart Button"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13690"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + <group value="paypal"/> + </annotations> + <before> + + <!-- Create Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Set Paypal express config --> + <magentoCLI command="config:set {{StorefrontPaypalEnableConfigData.path}} {{StorefrontPaypalEnableConfigData.value}}" stepKey="enablePaypal"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableInContextCheckoutConfigData.path}} {{StorefrontPaypalEnableInContextCheckoutConfigData.value}}" stepKey="enableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{_CREDS.magento/paypal_express_checkout_us_merchant_id}}" stepKey="setMerchantId"/> + <createData entity="PaypalConfig" stepKey="createPaypalExpressConfig"/> + + <!-- Login --> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + <after> + <!-- Cleanup Paypal configurations --> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{StorefrontPaypalMerchantAccountIdConfigData.value}}" stepKey="deleteMerchantId"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> + <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> + + <!-- Delete product --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete customer --> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Place an order using PayPal payment method --> + <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> + <argument name="Category" value="$$createCategory$$"/> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!-- PayPal checkout --> + <actionGroup ref="StorefrontPayOrderOnPayPalCheckoutActionGroup" stepKey="payOrderOnPayPalCheckout"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php deleted file mode 100644 index e4de60cafb8ad..0000000000000 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Fieldset; - -/** - * Class GroupTest - */ -class GroupTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Group - */ - protected $_model; - - /** - * @var \Magento\Framework\Data\Form\Element\AbstractElement - */ - protected $_element; - - /** - * @var \Magento\Backend\Model\Auth\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_authSession; - - /** - * @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_user; - - /** - * @var \Magento\Config\Model\Config\Structure\Element\Group|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_group; - - protected function setUp() - { - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); - $this->_element = $this->getMockForAbstractClass( - \Magento\Framework\Data\Form\Element\AbstractElement::class, - [], - '', - false, - true, - true, - ['getHtmlId', 'getElementHtml', 'getName', 'getElements', 'getId'] - ); - $this->_element->expects($this->any()) - ->method('getHtmlId') - ->will($this->returnValue('html id')); - $this->_element->expects($this->any()) - ->method('getElementHtml') - ->will($this->returnValue('element html')); - $this->_element->expects($this->any()) - ->method('getName') - ->will($this->returnValue('name')); - $this->_element->expects($this->any()) - ->method('getElements') - ->will($this->returnValue([])); - $this->_element->expects($this->any()) - ->method('getId') - ->will($this->returnValue('id')); - $this->_user = $this->createMock(\Magento\User\Model\User::class); - $this->_authSession = $this->createMock(\Magento\Backend\Model\Auth\Session::class); - $this->_authSession->expects($this->any()) - ->method('__call') - ->with('getUser') - ->will($this->returnValue($this->_user)); - $this->_model = $helper->getObject( - \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class, - ['authSession' => $this->_authSession] - ); - $this->_model->setGroup($this->_group); - } - - /** - * @param mixed $expanded - * @param int $expected - * @dataProvider isCollapseStateDataProvider - */ - public function testIsCollapseState($expanded, $expected) - { - $this->_user->setExtra(['configState' => []]); - $this->_element->setGroup(isset($expanded) ? ['expanded' => $expanded] : []); - $html = $this->_model->render($this->_element); - $this->assertContains( - '<input id="' . $this->_element->getHtmlId() . '-state" name="config_state[' - . $this->_element->getId() . ']" type="hidden" value="' . $expected . '" />', - $html - ); - } - - /** - * @return array - */ - public function isCollapseStateDataProvider() - { - return [ - [null, 0], - [false, 0], - ['', 0], - [1, 1], - ['1', 1], - ]; - } -} diff --git a/app/code/Magento/Paypal/etc/config.xml b/app/code/Magento/Paypal/etc/config.xml index 4619fc8539442..6c0601f80137d 100644 --- a/app/code/Magento/Paypal/etc/config.xml +++ b/app/code/Magento/Paypal/etc/config.xml @@ -164,7 +164,6 @@ <title>Credit Card (Payflow Advanced) PayPal PayPal - PayPal 1 1 GET diff --git a/app/code/Magento/Paypal/etc/db_schema.xml b/app/code/Magento/Paypal/etc/db_schema.xml index 4dc283fcb48ce..3300f3754e656 100644 --- a/app/code/Magento/Paypal/etc/db_schema.xml +++ b/app/code/Magento/Paypal/etc/db_schema.xml @@ -9,17 +9,17 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Agreement ID"/> + comment="Customer ID"/> - + + comment="Store ID"/> @@ -40,9 +40,9 @@
+ comment="Agreement ID"/> + comment="Order ID"/> @@ -59,9 +59,9 @@
+ comment="Report ID"/> - + @@ -75,15 +75,15 @@
+ comment="Row ID"/> - - + comment="Report ID"/> + + + comment="Paypal Reference ID"/> + comment="Paypal Reference ID Type"/> - + @@ -117,9 +117,9 @@
+ comment="Cert ID"/> + default="0" comment="Website ID"/> @@ -135,7 +135,7 @@ comment="PayPal Payflow Link Payment Transaction"> - + @@ -146,11 +146,11 @@
- + + comment="Paypal Correlation ID"/>
getEvent()->getControllerAction(); if ($action instanceof \Magento\Persistent\Controller\Index) { - if ((($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - || $this->_persistentData->isShoppingCartPersist()) + if (($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) + || $this->_persistentData->isShoppingCartPersist() ) { $this->quoteManager->setGuest(true); } diff --git a/app/code/Magento/Persistent/etc/db_schema.xml b/app/code/Magento/Persistent/etc/db_schema.xml index 5021d240417b2..e14dedba0ed56 100644 --- a/app/code/Magento/Persistent/etc/db_schema.xml +++ b/app/code/Magento/Persistent/etc/db_schema.xml @@ -9,10 +9,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Session ID"/> + comment="Customer ID"/> diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml index 16b44c502fc47..7466fbe990b02 100644 --- a/app/code/Magento/Persistent/etc/frontend/sections.xml +++ b/app/code/Magento/Persistent/etc/frontend/sections.xml @@ -10,4 +10,7 @@
+ +
+ diff --git a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml index 0447b3e1b9cef..ba55895453a09 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml @@ -1,4 +1,5 @@
getRandomString(10); ?> - isRememberMeChecked()) : ?> checked="checked" title="escapeHtmlAttr(__('Remember Me')) ?>" /> + isRememberMeChecked()) : ?> checked="checked" title="escapeHtmlAttr(__('Remember Me')) ?>" /> - escapeHtml(__('What\'s this?')) ?> - - escapeHtml(__('Check "Remember Me" to access your shopping cart on this computer even if you are not signed in.')) ?> - + escapeHtml(__('What\'s this?')) ?> + escapeHtml(__('Check "Remember Me" to access your shopping cart on this computer even if you are not signed in.')) ?>
diff --git a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html index f5dd1ffd57d9d..427e6bdcd63ab 100644 --- a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html +++ b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html @@ -9,8 +9,7 @@ - - - + + diff --git a/app/code/Magento/ProductAlert/Controller/Add/Price.php b/app/code/Magento/ProductAlert/Controller/Add/Price.php index 973db8c3bf5d4..2dbcc27cd57d9 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Price.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Price.php @@ -6,16 +6,17 @@ namespace Magento\ProductAlert\Controller\Add; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\ProductAlert\Controller\Add as AddController; -use Magento\Framework\App\Action\Context; -use Magento\Customer\Model\Session as CustomerSession; -use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\UrlInterface; +use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\ProductAlert\Controller\Add as AddController; +use Magento\Store\Model\StoreManagerInterface; /** * Controller for notifying about price. @@ -28,15 +29,17 @@ class Price extends AddController implements HttpGetActionInterface protected $storeManager; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var \Magento\Catalog\Api\ProductRepositoryInterface */ protected $productRepository; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * Price constructor. + * + * @param Context $context + * @param CustomerSession $customerSession + * @param StoreManagerInterface $storeManager + * @param ProductRepositoryInterface $productRepository */ public function __construct( Context $context, @@ -54,6 +57,7 @@ public function __construct( * * @param string $url * @return bool + * @throws NoSuchEntityException */ protected function isInternal($url) { @@ -68,13 +72,14 @@ protected function isInternal($url) /** * Method for adding info about product alert price. * - * @return \Magento\Framework\Controller\Result\Redirect + * @return \Magento\Framework\Controller\ResultInterface + * @throws NoSuchEntityException */ public function execute() { $backUrl = $this->getRequest()->getParam(Action::PARAM_NAME_URL_ENCODED); $productId = (int)$this->getRequest()->getParam('product_id'); - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!$backUrl || !$productId) { $resultRedirect->setPath('/'); @@ -93,9 +98,9 @@ public function execute() ->setWebsiteId($store->getWebsiteId()) ->setStoreId($store->getId()); $model->save(); - $this->messageManager->addSuccess(__('You saved the alert subscription.')); + $this->messageManager->addSuccessMessage(__('You saved the alert subscription.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('There are not enough parameters.')); + $this->messageManager->addErrorMessage(__('There are not enough parameters.')); if ($this->isInternal($backUrl)) { $resultRedirect->setUrl($backUrl); } else { @@ -103,7 +108,7 @@ public function execute() } return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Add/Stock.php b/app/code/Magento/ProductAlert/Controller/Add/Stock.php index f36fdf5fb715e..38b22d1efb852 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Stock.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Stock.php @@ -76,13 +76,13 @@ public function execute() ->setWebsiteId($store->getWebsiteId()) ->setStoreId($store->getId()); $model->save(); - $this->messageManager->addSuccess(__('Alert subscription has been saved.')); + $this->messageManager->addSuccessMessage(__('Alert subscription has been saved.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('There are not enough parameters.')); + $this->messageManager->addErrorMessage(__('There are not enough parameters.')); $resultRedirect->setUrl($backUrl); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php index 2077b1ff2794b..0aabf5d96e1e7 100644 --- a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php +++ b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Price.php @@ -6,6 +6,7 @@ namespace Magento\ProductAlert\Controller\Unsubscribe; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\ProductAlert\Controller\Unsubscribe as UnsubscribeController; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session as CustomerSession; @@ -13,7 +14,10 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\NoSuchEntityException; -class Price extends UnsubscribeController +/** + * Class Price + */ +class Price extends UnsubscribeController implements HttpGetActionInterface { /** * @var \Magento\Catalog\Api\ProductRepositoryInterface @@ -35,6 +39,8 @@ public function __construct( } /** + * Unsubscribe action + * * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() @@ -51,8 +57,13 @@ public function execute() /* @var $product \Magento\Catalog\Model\Product */ $product = $this->productRepository->getById($productId); if (!$product->isVisibleInCatalog()) { - throw new NoSuchEntityException(); + $this->messageManager->addErrorMessage( + __("The product wasn't found. Verify the product and try again.") + ); + $resultRedirect->setPath('customer/account/'); + return $resultRedirect; } + /** @var \Magento\ProductAlert\Model\Price $model */ $model = $this->_objectManager->create(\Magento\ProductAlert\Model\Price::class) ->setCustomerId($this->customerSession->getCustomerId()) @@ -67,13 +78,13 @@ public function execute() $model->delete(); } - $this->messageManager->addSuccess(__('You deleted the alert subscription.')); + $this->messageManager->addSuccessMessage(__('You deleted the alert subscription.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__("The product wasn't found. Verify the product and try again.")); + $this->messageManager->addErrorMessage(__("The product wasn't found. Verify the product and try again.")); $resultRedirect->setPath('customer/account/'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php index 1b729f988e4a5..f8df6ae6af38d 100644 --- a/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php +++ b/app/code/Magento/ProductAlert/Controller/Unsubscribe/Stock.php @@ -94,13 +94,13 @@ public function execute() if ($model->getId()) { $model->delete(); } - $this->messageManager->addSuccess(__('You will no longer receive stock alerts for this product.')); + $this->messageManager->addSuccessMessage(__('You will no longer receive stock alerts for this product.')); } catch (NoSuchEntityException $noEntityException) { - $this->messageManager->addError(__('The product was not found.')); + $this->messageManager->addErrorMessage(__('The product was not found.')); $resultRedirect->setPath('customer/account/'); return $resultRedirect; } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("The alert subscription couldn't update at this time. Please try again later.") ); diff --git a/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php b/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php new file mode 100644 index 0000000000000..a07c93337c041 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Unit/Controller/Unsubscribe/PriceTest.php @@ -0,0 +1,125 @@ +objectManager = new ObjectManager($this); + $this->requestMock = $this->createMock(Http::class); + $this->resultFactoryMock = $this->createMock(ResultFactory::class); + $this->resultRedirectMock = $this->createMock(Redirect::class); + $this->messageManagerMock = $this->createMock(Manager::class); + $this->productMock = $this->createMock(Product::class); + $this->contextMock = $this->createMock(Context::class); + $this->customerSessionMock = $this->createMock(Session::class); + $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->with(ResultFactory::TYPE_REDIRECT) + ->willReturn($this->resultRedirectMock); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); + $this->contextMock->expects($this->any())->method('getResultFactory')->willReturn($this->resultFactoryMock); + $this->contextMock->expects($this->any())->method('getMessageManager')->willReturn($this->messageManagerMock); + + $this->priceController = $this->objectManager->getObject( + Price::class, + [ + 'context' => $this->contextMock, + 'customerSession' => $this->customerSessionMock, + 'productRepository' => $this->productRepositoryMock, + ] + ); + } + + public function testProductIsNotVisibleInCatalog() + { + $productId = 123; + $this->requestMock->expects($this->any())->method('getParam')->with('product')->willReturn($productId); + $this->productRepositoryMock->expects($this->any()) + ->method('getById') + ->with($productId) + ->willReturn($this->productMock); + $this->productMock->expects($this->any())->method('isVisibleInCatalog')->willReturn(false); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with(__("The product wasn't found. Verify the product and try again.")); + $this->resultRedirectMock->expects($this->once())->method('setPath')->with('customer/account/'); + + $this->assertEquals( + $this->resultRedirectMock, + $this->priceController->execute() + ); + } +} diff --git a/app/code/Magento/ProductAlert/etc/db_schema.xml b/app/code/Magento/ProductAlert/etc/db_schema.xml index cb91560f8daa6..17cc76246e5c6 100644 --- a/app/code/Magento/ProductAlert/etc/db_schema.xml +++ b/app/code/Magento/ProductAlert/etc/db_schema.xml @@ -9,17 +9,17 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Product alert price ID"/> + default="0" comment="Customer ID"/> + default="0" comment="Product ID"/> + default="0" comment="Website ID"/> + default="0" comment="Store ID"/>
+ comment="Product alert stock ID"/> + default="0" comment="Customer ID"/> + default="0" comment="Product ID"/> + default="0" comment="Website ID"/> + default="0" comment="Store ID"/> setData(self::KEY_ITEMS, $items); + } + + /** + * @inheritdoc + */ + public function getItems() + { + return $this->_get(self::KEY_ITEMS) === null ? [] : $this->_get(self::KEY_ITEMS); + } + + /** + * @inheritdoc + */ + public function getSearchCriteria() + { + return $this->_get(self::KEY_SEARCH_CRITERIA); + } + + /** + * @inheritdoc + */ + public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) + { + return $this->setData(self::KEY_SEARCH_CRITERIA, $searchCriteria); + } + + /** + * @inheritdoc + */ + public function getTotalCount() + { + return $this->_get(self::KEY_TOTAL_COUNT); + } + + /** + * @inheritdoc + */ + public function setTotalCount($count) + { + return $this->setData(self::KEY_TOTAL_COUNT, $count); + } +} diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 00060c15c10d8..d8dd0953407d4 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -168,7 +168,7 @@ public function getAllBaseTotalAmounts() { return $this->baseTotalAmounts; } - + //@codeCoverageIgnoreEnd /** diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index 77dfec9603a5c..a1228903e2323 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -113,6 +113,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) $customerCountryCode = $customerAddress->getCountryId(); $customerVatNumber = $customerAddress->getVatId(); + $address->setCountryId($customerCountryCode); + $address->setVatId($customerVatNumber); } $groupId = null; diff --git a/app/code/Magento/Quote/Observer/SubmitObserver.php b/app/code/Magento/Quote/Observer/SubmitObserver.php index 1213636e5966b..0d6613a691390 100644 --- a/app/code/Magento/Quote/Observer/SubmitObserver.php +++ b/app/code/Magento/Quote/Observer/SubmitObserver.php @@ -5,13 +5,21 @@ */ namespace Magento\Quote\Observer; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Model\Quote; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Psr\Log\LoggerInterface; +/** + * Class SubmitObserver + */ class SubmitObserver implements ObserverInterface { /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; @@ -21,27 +29,37 @@ class SubmitObserver implements ObserverInterface private $orderSender; /** - * @param \Psr\Log\LoggerInterface $logger + * @var InvoiceSender + */ + private $invoiceSender; + + /** + * @param LoggerInterface $logger * @param OrderSender $orderSender + * @param InvoiceSender $invoiceSender */ public function __construct( - \Psr\Log\LoggerInterface $logger, - OrderSender $orderSender + LoggerInterface $logger, + OrderSender $orderSender, + InvoiceSender $invoiceSender ) { $this->logger = $logger; $this->orderSender = $orderSender; + $this->invoiceSender = $invoiceSender; } /** - * @param \Magento\Framework\Event\Observer $observer + * Sends emails to customer. + * + * @param Observer $observer * * @return void */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { - /** @var \Magento\Quote\Model\Quote $quote */ + /** @var Quote $quote */ $quote = $observer->getEvent()->getQuote(); - /** @var \Magento\Sales\Model\Order $order */ + /** @var Order $order */ $order = $observer->getEvent()->getOrder(); /** @@ -51,6 +69,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$redirectUrl && $order->getCanSendNewEmailFlag()) { try { $this->orderSender->send($order); + $invoice = current($order->getInvoiceCollection()->getItems()); + if ($invoice) { + $this->invoiceSender->send($invoice); + } } catch (\Exception $e) { $this->logger->critical($e); } diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 4ec608a18a686..f7a4fda4f67d8 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -73,6 +73,9 @@ + + @@ -116,6 +119,7 @@ + @@ -143,6 +147,7 @@ + diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 4ea067c9be8f6..a590c8aa891a5 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -314,6 +314,43 @@ public function testDispatchWithCustomerCountryInEU() $this->model->execute($this->observerMock); } + public function testDispatchWithAddressCustomerVatIdAndCountryId() + { + $customerCountryCode = "BE"; + $customerVat = "123123123"; + $defaultShipping = 1; + + $customerAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $customerAddress->expects($this->any()) + ->method("getVatId") + ->willReturn($customerVat); + + $customerAddress->expects($this->any()) + ->method("getCountryId") + ->willReturn($customerCountryCode); + + $this->addressRepository->expects($this->once()) + ->method("getById") + ->with($defaultShipping) + ->willReturn($customerAddress); + + $this->customerMock->expects($this->atLeastOnce()) + ->method("getDefaultShipping") + ->willReturn($defaultShipping); + + $this->vatValidatorMock->expects($this->once()) + ->method('isEnabled') + ->with($this->quoteAddressMock, $this->storeId) + ->will($this->returnValue(true)); + + $this->customerVatMock->expects($this->once()) + ->method('isCountryInEU') + ->with($customerCountryCode) + ->willReturn(true); + + $this->model->execute($this->observerMock); + } + public function testDispatchWithEmptyShippingAddress() { $customerCountryCode = "DE"; diff --git a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php index c19606a7b8f5d..f06f5466df91f 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php @@ -5,75 +5,116 @@ */ namespace Magento\Quote\Test\Unit\Observer; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Payment; +use Magento\Quote\Observer\SubmitObserver; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection; +use Psr\Log\LoggerInterface; + +/** + * Class SubmitObserverTest + */ class SubmitObserverTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Quote\Observer\SubmitObserver + * @var SubmitObserver */ - protected $model; + private $model; /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $loggerMock; + private $loggerMock; /** - * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender|\PHPUnit_Framework_MockObject_MockObject + * @var OrderSender|\PHPUnit_Framework_MockObject_MockObject */ - protected $orderSenderMock; + private $orderSenderMock; /** - * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject + * @var InvoiceSender|\PHPUnit_Framework_MockObject_MockObject */ - protected $observerMock; + private $invoiceSender; /** - * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject + * @var Observer|\PHPUnit_Framework_MockObject_MockObject */ - protected $quoteMock; + private $observerMock; /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ - protected $orderMock; + private $quoteMock; /** - * @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject + * @var Order|\PHPUnit_Framework_MockObject_MockObject */ - protected $paymentMock; + private $orderMock; + + /** + * @var Payment|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentMock; protected function setUp() { - $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); - $this->paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); - $this->orderSenderMock = - $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderSender::class); - $eventMock = $this->getMockBuilder(\Magento\Framework\Event::class) + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderSenderMock = $this->createMock(OrderSender::class); + $this->invoiceSender = $this->createMock(InvoiceSender::class); + $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() ->setMethods(['getQuote', 'getOrder']) ->getMock(); - $this->observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']); + $this->observerMock = $this->createPartialMock(Observer::class, ['getEvent']); $this->observerMock->expects($this->any())->method('getEvent')->willReturn($eventMock); $eventMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock); $eventMock->expects($this->once())->method('getOrder')->willReturn($this->orderMock); $this->quoteMock->expects($this->once())->method('getPayment')->willReturn($this->paymentMock); - $this->model = new \Magento\Quote\Observer\SubmitObserver( + $this->model = new SubmitObserver( $this->loggerMock, - $this->orderSenderMock + $this->orderSenderMock, + $this->invoiceSender ); } + /** + * Tests successful email sending. + */ public function testSendEmail() { - $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(''); - $this->orderMock->expects($this->once())->method('getCanSendNewEmailFlag')->willReturn(true); - $this->orderSenderMock->expects($this->once())->method('send')->willReturn(true); - $this->loggerMock->expects($this->never())->method('critical'); + $this->paymentMock->method('getOrderPlaceRedirectUrl')->willReturn(''); + $invoice = $this->createMock(Invoice::class); + $invoiceCollection = $this->createMock(Collection::class); + $invoiceCollection->method('getItems') + ->willReturn([$invoice]); + + $this->orderMock->method('getInvoiceCollection') + ->willReturn($invoiceCollection); + $this->orderMock->method('getCanSendNewEmailFlag')->willReturn(true); + $this->orderSenderMock->expects($this->once()) + ->method('send')->willReturn(true); + $this->invoiceSender->expects($this->once()) + ->method('send') + ->with($invoice) + ->willReturn(true); + $this->loggerMock->expects($this->never()) + ->method('critical'); + $this->model->execute($this->observerMock); } + /** + * Tests failing email sending. + */ public function testFailToSendEmail() { $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(''); @@ -85,6 +126,9 @@ public function testFailToSendEmail() $this->model->execute($this->observerMock); } + /** + * Tests send email when redirect. + */ public function testSendEmailWhenRedirectUrlExists() { $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(false); diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index b4c75fc1d21d0..d41591c619cde 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -11,7 +11,7 @@ + default="0" comment="Store ID"/> @@ -27,7 +27,7 @@ + default="0" comment="Orig Order ID"/> + comment="Customer ID"/> + identity="false" comment="Customer Tax Class ID"/> + default="0" comment="Customer Group ID"/> @@ -63,7 +63,7 @@ identity="false" default="0" comment="Customer Is Guest"/> - +
+ comment="Address ID"/> + default="0" comment="Quote ID"/> + comment="Customer ID"/> + comment="Customer Address ID"/> @@ -129,9 +129,9 @@ + comment="Region ID"/> - + - + - + @@ -215,19 +215,19 @@
+ comment="Item ID"/> + default="0" comment="Quote ID"/> + comment="Product ID"/> + comment="Store ID"/> + comment="Parent Item ID"/> @@ -315,13 +315,13 @@
+ comment="Address Item ID"/> + comment="Parent Item ID"/> + default="0" comment="Quote Address ID"/> + default="0" comment="Quote Item ID"/> + comment="Product ID"/> + comment="Super Product ID"/> + comment="Parent Product ID"/> + comment="Store ID"/> @@ -413,11 +413,11 @@
+ comment="Option ID"/> + comment="Item ID"/> + comment="Product ID"/> @@ -431,9 +431,9 @@
+ comment="Payment ID"/> + default="0" comment="Quote ID"/>
+ comment="Rate ID"/> + default="0" comment="Address ID"/> - + diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php new file mode 100644 index 0000000000000..5c773d44e6a1d --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/SaveQuoteAddressToCustomerAddressBook.php @@ -0,0 +1,96 @@ +addressFactory = $addressFactory; + $this->addressRepository = $addressRepository; + $this->regionFactory = $regionFactory; + } + + /** + * Save Address to Customer Address Book. + * + * @param QuoteAddress $quoteAddress + * @param int $customerId + * + * @return void + * @throws GraphQlInputException + */ + public function execute(QuoteAddress $quoteAddress, int $customerId): void + { + try { + /** @var AddressInterface $customerAddress */ + $customerAddress = $this->addressFactory->create(); + $customerAddress->setFirstname($quoteAddress->getFirstname()) + ->setLastname($quoteAddress->getLastname()) + ->setMiddlename($quoteAddress->getMiddlename()) + ->setPrefix($quoteAddress->getPrefix()) + ->setSuffix($quoteAddress->getSuffix()) + ->setVatId($quoteAddress->getVatId()) + ->setCountryId($quoteAddress->getCountryId()) + ->setCompany($quoteAddress->getCompany()) + ->setRegionId($quoteAddress->getRegionId()) + ->setFax($quoteAddress->getFax()) + ->setCity($quoteAddress->getCity()) + ->setPostcode($quoteAddress->getPostcode()) + ->setStreet($quoteAddress->getStreet()) + ->setTelephone($quoteAddress->getTelephone()) + ->setCustomerId($customerId); + + /** @var RegionInterface $region */ + $region = $this->regionFactory->create(); + $region->setRegionCode($quoteAddress->getRegionCode()) + ->setRegion($quoteAddress->getRegion()) + ->setRegionId($quoteAddress->getRegionId()); + $customerAddress->setRegion($region); + + $this->addressRepository->save($customerAddress); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/DiscountAggregator.php b/app/code/Magento/QuoteGraphQl/Model/Cart/DiscountAggregator.php new file mode 100644 index 0000000000000..a620b4b2610cf --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/DiscountAggregator.php @@ -0,0 +1,48 @@ +getItems(); + $discountPerRule = []; + foreach ($items as $item) { + $discountBreakdown = $item->getExtensionAttributes()->getDiscounts(); + if ($discountBreakdown) { + foreach ($discountBreakdown as $key => $value) { + /* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discount */ + $discount = $value['discount']; + $rule = $value['rule']; + if (isset($discountPerRule[$key])) { + $discountPerRule[$key]['discount'] += $discount->getAmount(); + } else { + $discountPerRule[$key]['discount'] = $discount->getAmount(); + } + $discountPerRule[$key]['rule'] = $rule; + } + } + } + return $discountPerRule; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php new file mode 100644 index 0000000000000..2a57c281de183 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetShippingAddress.php @@ -0,0 +1,120 @@ +quoteAddressFactory = $quoteAddressFactory; + $this->saveQuoteAddressToCustomerAddressBook = $saveQuoteAddressToCustomerAddressBook; + } + + /** + * Get Shipping Address based on the input. + * + * @param ContextInterface $context + * @param array $shippingAddressInput + * @return Address + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, array $shippingAddressInput): Address + { + $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; + $addressInput = $shippingAddressInput['address'] ?? null; + + if ($addressInput) { + $addressInput['customer_notes'] = $shippingAddressInput['customer_notes'] ?? ''; + } + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The shipping address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + $shippingAddress = $this->createShippingAddress($context, $customerAddressId, $addressInput); + + return $shippingAddress; + } + + /** + * Create shipping address. + * + * @param ContextInterface $context + * @param int|null $customerAddressId + * @param array|null $addressInput + * + * @return \Magento\Quote\Model\Quote\Address + * @throws GraphQlAuthorizationException + */ + private function createShippingAddress( + ContextInterface $context, + ?int $customerAddressId, + ?array $addressInput + ) { + $customerId = $context->getUserId(); + + if (null === $customerAddressId) { + $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + + // need to save address only for registered user and if save_in_address_book = true + if (0 !== $customerId + && isset($addressInput['save_in_address_book']) + && (bool)$addressInput['save_in_address_book'] === true + ) { + $this->saveQuoteAddressToCustomerAddressBook->execute($shippingAddress, $customerId); + } + } else { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + $customerId + ); + } + + return $shippingAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php index b90752ae7358b..45713f9372e7b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -13,6 +13,7 @@ use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Quote\Address; +use Magento\QuoteGraphQl\Model\Cart\Address\SaveQuoteAddressToCustomerAddressBook; /** * Set billing address for a specified shopping cart @@ -29,16 +30,24 @@ class SetBillingAddressOnCart */ private $assignBillingAddressToCart; + /** + * @var SaveQuoteAddressToCustomerAddressBook + */ + private $saveQuoteAddressToCustomerAddressBook; + /** * @param QuoteAddressFactory $quoteAddressFactory * @param AssignBillingAddressToCart $assignBillingAddressToCart + * @param SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook */ public function __construct( QuoteAddressFactory $quoteAddressFactory, - AssignBillingAddressToCart $assignBillingAddressToCart + AssignBillingAddressToCart $assignBillingAddressToCart, + SaveQuoteAddressToCustomerAddressBook $saveQuoteAddressToCustomerAddressBook ) { $this->quoteAddressFactory = $quoteAddressFactory; $this->assignBillingAddressToCart = $assignBillingAddressToCart; + $this->saveQuoteAddressToCustomerAddressBook = $saveQuoteAddressToCustomerAddressBook; } /** @@ -103,6 +112,15 @@ private function createBillingAddress( ): Address { if (null === $customerAddressId) { $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + + $customerId = $context->getUserId(); + // need to save address only for registered user and if save_in_address_book = true + if (0 !== $customerId + && isset($addressInput['save_in_address_book']) + && (bool)$addressInput['save_in_address_book'] === true + ) { + $this->saveQuoteAddressToCustomerAddressBook->execute($billingAddress, $customerId); + } } else { if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); @@ -113,6 +131,7 @@ private function createBillingAddress( (int)$context->getUserId() ); } + return $billingAddress; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 260f1343556f0..6b1296eaf3752 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -7,7 +7,6 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; @@ -18,25 +17,25 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface { /** - * @var QuoteAddressFactory + * @var AssignShippingAddressToCart */ - private $quoteAddressFactory; + private $assignShippingAddressToCart; /** - * @var AssignShippingAddressToCart + * @var GetShippingAddress */ - private $assignShippingAddressToCart; + private $getShippingAddress; /** - * @param QuoteAddressFactory $quoteAddressFactory * @param AssignShippingAddressToCart $assignShippingAddressToCart + * @param GetShippingAddress $getShippingAddress */ public function __construct( - QuoteAddressFactory $quoteAddressFactory, - AssignShippingAddressToCart $assignShippingAddressToCart + AssignShippingAddressToCart $assignShippingAddressToCart, + GetShippingAddress $getShippingAddress ) { - $this->quoteAddressFactory = $quoteAddressFactory; $this->assignShippingAddressToCart = $assignShippingAddressToCart; + $this->getShippingAddress = $getShippingAddress; } /** @@ -50,37 +49,8 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s ); } $shippingAddressInput = current($shippingAddressesInput); - $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - $addressInput = $shippingAddressInput['address'] ?? null; - if ($addressInput) { - $addressInput['customer_notes'] = $shippingAddressInput['customer_notes'] ?? ''; - } - - if (null === $customerAddressId && null === $addressInput) { - throw new GraphQlInputException( - __('The shipping address must contain either "customer_address_id" or "address".') - ); - } - - if ($customerAddressId && $addressInput) { - throw new GraphQlInputException( - __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') - ); - } - - if (null === $customerAddressId) { - $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); - } else { - if (false === $context->getExtensionAttributes()->getIsCustomer()) { - throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); - } - - $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( - (int)$customerAddressId, - $context->getUserId() - ); - } + $shippingAddress = $this->getShippingAddress->execute($context, $shippingAddressInput); $this->assignShippingAddressToCart->execute($cart, $shippingAddress); } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php new file mode 100644 index 0000000000000..fa232f4d9cd6c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php @@ -0,0 +1,52 @@ +couponManagement = $couponManagement; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $cart = $value['model']; + $cartId = $cart->getId(); + $appliedCoupons = []; + $appliedCoupon = $this->couponManagement->get($cartId); + if ($appliedCoupon) { + $appliedCoupons[] = [ 'code' => $appliedCoupon ]; + } + return !empty($appliedCoupons) ? $appliedCoupons : null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index a591c74e78db3..b66327ac1dbba 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -55,7 +55,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value // But the totals should be calculated even if no address is set $this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote()); } - $currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode(); return [ @@ -71,6 +70,41 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value 'currency' => $currencyCode, 'value' => $cartItem->getRowTotalInclTax(), ], + 'total_item_discount' => [ + 'currency' => $currencyCode, + 'value' => $cartItem->getDiscountAmount(), + ], + 'discounts' => $this->getDiscountValues($cartItem, $currencyCode) ]; } + + /** + * Get Discount Values + * + * @param Item $cartItem + * @param string $currencyCode + * @return array + */ + private function getDiscountValues($cartItem, $currencyCode) + { + $itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts(); + if ($itemDiscounts) { + $discountValues=[]; + foreach ($itemDiscounts as $value) { + $discount = []; + $amount = []; + /* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */ + $discountData = $value['discount']; + /* @var \Magento\SalesRule\Model\Rule $rule */ + $rule = $value['rule']; + $discount['label'] = $rule->getStoreLabel($cartItem->getQuote()->getStore()) ?: __('Discount'); + $amount['value'] = $discountData->getAmount(); + $amount['currency'] = $currencyCode; + $discount['amount'] = $amount; + $discountValues[] = $discount; + } + return $discountValues; + } + return null; + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php new file mode 100644 index 0000000000000..2c3f6d0e69f4a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php @@ -0,0 +1,75 @@ +discountAggregator = $discountAggregator; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $quote = $value['model']; + + return $this->getDiscountValues($quote); + } + + /** + * Get Discount Values + * + * @param Quote $quote + * @return array + */ + private function getDiscountValues(Quote $quote) + { + $discountValues=[]; + $totalDiscounts = $this->discountAggregator->aggregateDiscountPerRule($quote); + if ($totalDiscounts) { + foreach ($totalDiscounts as $value) { + $discount = []; + $amount = []; + /* @var \Magento\SalesRule\Model\Rule $rule*/ + $rule = $value['rule']; + $discount['label'] = $rule->getStoreLabel($quote->getStore()) ?: __('Discount'); + $amount['value'] = $value['discount']; + $amount['currency'] = $quote->getQuoteCurrencyCode(); + $discount['amount'] = $amount; + $discountValues[] = $discount; + } + return $discountValues; + } + return null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php index 1a0740a75c8f8..3a10c773c5f22 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php @@ -89,6 +89,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'order' => [ + 'order_number' => $order->getIncrementId(), + // @deprecated The order_id field is deprecated, use order_number instead 'order_id' => $order->getIncrementId(), ], ]; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index 03e1e6ffe822d..dd4ce8fe7f7a6 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -99,6 +99,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'order' => [ + 'order_number' => $order->getIncrementId(), + // @deprecated The order_id field is deprecated, use order_number instead 'order_id' => $order->getIncrementId(), ], ]; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php index c6f25dd78823b..429fda816efd3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -31,36 +31,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Address $address */ $address = $value['model']; $rates = $address->getAllShippingRates(); - $carrierTitle = null; - $methodTitle = null; + $carrierTitle = ''; + $methodTitle = ''; - if (count($rates) > 0 && !empty($address->getShippingMethod())) { - list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); + if (!count($rates) || empty($address->getShippingMethod())) { + return null; + } - /** @var Rate $rate */ - foreach ($rates as $rate) { - if ($rate->getCode() == $address->getShippingMethod()) { - $carrierTitle = $rate->getCarrierTitle(); - $methodTitle = $rate->getMethodTitle(); - break; - } - } + list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); - $data = [ - 'carrier_code' => $carrierCode, - 'method_code' => $methodCode, - 'carrier_title' => $carrierTitle, - 'method_title' => $methodTitle, - 'amount' => [ - 'value' => $address->getShippingAmount(), - 'currency' => $address->getQuote()->getQuoteCurrencyCode(), - ], - /** @deprecated The field should not be used on the storefront */ - 'base_amount' => null, - ]; - } else { - $data = null; + /** @var Rate $rate */ + foreach ($rates as $rate) { + if ($rate->getCode() == $address->getShippingMethod()) { + $carrierTitle = $rate->getCarrierTitle(); + $methodTitle = $rate->getMethodTitle(); + break; + } } + + $data = [ + 'carrier_code' => $carrierCode, + 'method_code' => $methodCode, + 'carrier_title' => $carrierTitle, + 'method_title' => $methodTitle, + 'amount' => [ + 'value' => $address->getShippingAmount(), + 'currency' => $address->getQuote()->getQuoteCurrencyCode(), + ], + /** @deprecated The field should not be used on the storefront */ + 'base_amount' => null, + ]; + return $data; } } diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 1c60f18c5bc26..51fdf4299fadc 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -110,7 +110,7 @@ input CartAddressInput { postcode: String country_code: String! telephone: String! - save_in_address_book: Boolean! + save_in_address_book: Boolean } input SetShippingMethodsOnCartInput { @@ -151,9 +151,10 @@ type CartPrices { grand_total: Money subtotal_including_tax: Money subtotal_excluding_tax: Money - discount: CartDiscount + discount: CartDiscount @deprecated(reason: "Use discounts instead ") subtotal_with_discount_excluding_tax: Money applied_taxes: [CartTaxItem] + discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts") } type CartTaxItem { @@ -192,7 +193,8 @@ type PlaceOrderOutput { type Cart { items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") - applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") billing_address: BillingCartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") @@ -243,11 +245,11 @@ type CartAddressCountry { } type SelectedShippingMethod { - carrier_code: String - method_code: String - carrier_title: String - method_title: String - amount: Money + carrier_code: String! + method_code: String! + carrier_title: String! + method_title: String! + amount: Money! base_amount: Money @deprecated(reason: "The field should not be used on the storefront") } @@ -322,10 +324,17 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ product: ProductInterface! } +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") { + amount: Money! @doc(description:"The amount of the discount") + label: String! @doc(description:"A description of the discount") +} + type CartItemPrices { price: Money! row_total: Money! row_total_including_tax: Money! + discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") + total_item_discount: Money @doc(description:"The total of all discounts applied to the item") } type SelectedCustomizableOption { @@ -350,5 +359,6 @@ type CartItemSelectedOptionValuePrice { } type Order { - order_id: String + order_number: String! + order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php index 813c5f28bf4d9..a7f619863af56 100644 --- a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php +++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php @@ -12,6 +12,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Backend\Model\Auth\Session; use Magento\Framework\App\CacheInterface; +use Magento\User\Model\User; /** * Class CanViewNotificationTest @@ -36,6 +37,11 @@ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase /** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */ private $cacheStorageMock; + /** + * @var User|\PHPUnit_Framework_MockObject_MockObject + */ + private $userMock; + public function setUp() { $this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class) @@ -44,7 +50,6 @@ public function setUp() ->getMock(); $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() - ->setMethods(['getUser', 'getId']) ->getMock(); $this->viewerLoggerMock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() @@ -52,6 +57,7 @@ public function setUp() $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->userMock = $this->createMock(User::class); $objectManager = new ObjectManager($this); $this->canViewNotification = $objectManager->getObject( CanViewNotification::class, @@ -68,8 +74,8 @@ public function testIsVisibleLoadDataFromCache() { $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once()) + ->willReturn($this->userMock); + $this->userMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->cacheStorageMock->expects($this->once()) @@ -93,8 +99,8 @@ public function testIsVisible($expected, $version, $lastViewVersion) ->willReturn(false); $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once()) + ->willReturn($this->userMock); + $this->userMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->productMetadataMock->expects($this->once()) diff --git a/app/code/Magento/Reports/etc/db_schema.xml b/app/code/Magento/Reports/etc/db_schema.xml index 1321ebba4d3d6..30accf36a053e 100644 --- a/app/code/Magento/Reports/etc/db_schema.xml +++ b/app/code/Magento/Reports/etc/db_schema.xml @@ -10,15 +10,15 @@
+ comment="Index ID"/> + comment="Visitor ID"/> + comment="Customer ID"/> + comment="Product ID"/> + comment="Store ID"/> @@ -54,15 +54,15 @@
+ comment="Index ID"/> + comment="Visitor ID"/> + comment="Customer ID"/> + comment="Product ID"/> + comment="Store ID"/> @@ -97,7 +97,7 @@
+ comment="Event Type ID"/> @@ -107,19 +107,19 @@
+ comment="Event ID"/> + default="0" comment="Event Type ID"/> + default="0" comment="Object ID"/> + default="0" comment="Subject ID"/> + comment="Store ID"/> @@ -146,12 +146,12 @@
- + + comment="Store ID"/> + comment="Product ID"/> @@ -182,12 +182,12 @@
- + + comment="Store ID"/> + comment="Product ID"/> @@ -218,12 +218,12 @@
- + + comment="Store ID"/> + comment="Product ID"/> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml index 55ca286ad3d47..2d33ca3c6a872 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml @@ -40,6 +40,7 @@ accounts col-qty col-qty + 0 diff --git a/app/code/Magento/Review/etc/db_schema.xml b/app/code/Magento/Review/etc/db_schema.xml index d1090d413384b..7a451dbbbcf98 100644 --- a/app/code/Magento/Review/etc/db_schema.xml +++ b/app/code/Magento/Review/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Review entity ID"/> @@ -17,7 +17,7 @@
+ comment="Status ID"/> @@ -25,13 +25,13 @@
+ comment="Review ID"/> + default="0" comment="Entity ID"/> + default="0" comment="Product ID"/> @@ -53,16 +53,16 @@
+ comment="Review detail ID"/> + default="0" comment="Review ID"/> + default="0" comment="Store ID"/> + comment="Customer ID"/> @@ -85,11 +85,11 @@
+ comment="Summary review entity ID"/> + default="0" comment="Product ID"/> + default="0" comment="Entity type ID"/>
+ comment="Rating Option ID"/> + default="0" comment="Rating ID"/> @@ -177,20 +177,20 @@
+ comment="Vote ID"/> + default="0" comment="Vote option ID"/> + default="0" comment="Customer ID"/> + default="0" comment="Product ID"/> + default="0" comment="Rating ID"/> + comment="Review ID"/>
+ comment="Vote aggregation ID"/> + default="0" comment="Rating ID"/> + default="0" comment="Product ID"/> + default="0" comment="Store ID"/> @@ -242,9 +242,9 @@
+ default="0" comment="Rating ID"/> + default="0" comment="Store ID"/> @@ -259,9 +259,9 @@
+ default="0" comment="Rating ID"/> + default="0" comment="Store ID"/> diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index 6729fe722de56..d58af06da94cf 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Rule\Model\Condition; @@ -17,6 +18,7 @@ * @method setFormName() * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -390,7 +392,7 @@ public function getValueParsed() $value = reset($value); } if (!is_array($value) && $this->isArrayOperatorType() && $value) { - $value = preg_split('#\s*[,;]\s*#', $value, null, PREG_SPLIT_NO_EMPTY); + $value = preg_split('#\s*[,;]\s*#', (string) $value, -1, PREG_SPLIT_NO_EMPTY); } $this->setValueParsed($value); } @@ -419,8 +421,11 @@ public function getValue() { if ($this->getInputType() == 'date' && !$this->getIsValueParsed()) { // date format intentionally hard-coded + $date = $this->getData('value'); + $date = (\is_numeric($date) ? '@' : '') . $date; $this->setValue( - (new \DateTime($this->getData('value')))->format('Y-m-d H:i:s') + (new \DateTime($date, new \DateTimeZone((string) $this->_localeDate->getConfigTimezone()))) + ->format('Y-m-d H:i:s') ); $this->setIsValueParsed(true); } @@ -432,6 +437,7 @@ public function getValue() * * @return array|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function getValueName() { @@ -469,6 +475,7 @@ public function getValueName() } return $value; } + //phpcs:enable Generic.Metrics.NestingLevel /** * Get inherited conditions selectors @@ -674,6 +681,9 @@ public function getValueElement() $elementParams['placeholder'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; $elementParams['autocomplete'] = 'off'; $elementParams['readonly'] = 'true'; + $elementParams['value_name'] = + (new \DateTime($elementParams['value'], new \DateTimeZone($this->_localeDate->getConfigTimezone()))) + ->format('Y-m-d'); } return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__value', @@ -879,7 +889,7 @@ protected function _compareValues($validatedValue, $value, $strict = true) return $validatedValue == $value; } - $validatePattern = preg_quote($validatedValue, '~'); + $validatePattern = preg_quote((string) $validatedValue, '~'); if ($strict) { $validatePattern = '^' . $validatePattern . '$'; } diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php index 52653197e3981..0ba41af04a1b3 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/AbstractConditionTest.php @@ -55,14 +55,14 @@ public function validateAttributeDataProvider() ['0', '==', 1, false], ['1', '==', 1, true], ['x', '==', 'x', true], - ['x', '==', 0, false], + ['x', '==', '0', false], [1, '!=', 1, false], [0, '!=', 1, true], ['0', '!=', 1, true], ['1', '!=', 1, false], ['x', '!=', 'x', false], - ['x', '!=', 0, true], + ['x', '!=', '0', true], [1, '==', [1], true], [1, '!=', [1], false], @@ -164,15 +164,15 @@ public function validateAttributeArrayInputTypeDataProvider() [[1, 2, 3], '{}', '1', true, 'grid'], [[1, 2, 3], '{}', '8', false, 'grid'], - [[1, 2, 3], '{}', 5, false, 'grid'], + [[1, 2, 3], '{}', '5', false, 'grid'], [[1, 2, 3], '{}', [2, 3, 4], true, 'grid'], [[1, 2, 3], '{}', [4], false, 'grid'], [[3], '{}', [], false, 'grid'], [1, '{}', 1, false, 'grid'], [1, '!{}', [1, 2, 3], false, 'grid'], [[1], '{}', null, false, 'grid'], - [null, '{}', null, true, 'input'], - [null, '!{}', null, false, 'input'], + ['null', '{}', 'null', true, 'input'], + ['null', '!{}', 'null', false, 'input'], [null, '{}', [1], false, 'input'], [[1, 2, 3], '()', 1, true, 'select'], diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php index 12e59e63f6f7c..1efa149b390ef 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php @@ -136,4 +136,12 @@ public function getFormValues() { return $this->_getAddress()->getData(); } + + /** + * @inheritDoc + */ + protected function getAddressStoreId() + { + return $this->_getAddress()->getOrder()->getStoreId(); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index e4b9dd4c63b93..3fe943c1b194c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -271,21 +271,24 @@ protected function _prepareForm() $this->_form->setValues($this->getFormValues()); - if ($this->_form->getElement('country_id')->getValue()) { - $countryId = $this->_form->getElement('country_id')->getValue(); - $this->_form->getElement('country_id')->setValue(null); - foreach ($this->_form->getElement('country_id')->getValues() as $country) { + $countryElement = $this->_form->getElement('country_id'); + + $this->processCountryOptions($countryElement); + + if ($countryElement->getValue()) { + $countryId = $countryElement->getValue(); + $countryElement->setValue(null); + foreach ($countryElement->getValues() as $country) { if ($country['value'] == $countryId) { - $this->_form->getElement('country_id')->setValue($countryId); + $countryElement->setValue($countryId); } } } - if ($this->_form->getElement('country_id')->getValue() === null) { - $this->_form->getElement('country_id')->setValue( + if ($countryElement->getValue() === null) { + $countryElement->setValue( $this->directoryHelper->getDefaultCountry($this->getStore()) ); } - $this->processCountryOptions($this->_form->getElement('country_id')); // Set custom renderer for VAT field if needed $vatIdElement = $this->_form->getElement('vat_id'); if ($vatIdElement && $this->getDisplayVatValidationButton() !== false) { @@ -309,7 +312,7 @@ protected function _prepareForm() */ private function processCountryOptions(\Magento\Framework\Data\Form\Element\AbstractElement $countryElement) { - $storeId = $this->getBackendQuoteSession()->getStoreId(); + $storeId = $this->getAddressStoreId(); $options = $this->getCountriesCollection() ->loadByStore($storeId) ->toOptionArray(); @@ -388,4 +391,14 @@ public function getAddressAsString(\Magento\Customer\Api\Data\AddressInterface $ return $this->escapeHtml($result); } + + /** + * Return address store id. + * + * @return int + */ + protected function getAddressStoreId() + { + return $this->getBackendQuoteSession()->getStoreId(); + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 9a271f741edda..001c581dc0dac 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,8 +5,7 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; -use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection - as ProductCollectionDataProvider; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection; use Magento\Framework\App\ObjectManager; /** @@ -48,7 +47,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended protected $_productFactory; /** - * @var ProductCollectionDataProvider $productCollectionProvider + * @var ProductCollection $productCollectionProvider */ private $productCollectionProvider; @@ -60,7 +59,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data - * @param ProductCollectionDataProvider|null $productCollectionProvider + * @param ProductCollection|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -70,14 +69,14 @@ public function __construct( \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, array $data = [], - ProductCollectionDataProvider $productCollectionProvider = null + ProductCollection $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; $this->productCollectionProvider = $productCollectionProvider - ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); + ?: ObjectManager::getInstance()->get(ProductCollection::class); parent::__construct($context, $backendHelper, $data); } @@ -94,6 +93,7 @@ protected function _construct() $this->setCheckboxCheckCallback('order.productGridCheckboxCheck.bind(order)'); $this->setRowInitCallback('order.productGridRowInit.bind(order)'); $this->setDefaultSort('entity_id'); + $this->setFilterKeyPressCallback('order.productGridFilterKeyPress'); $this->setUseAjax(true); if ($this->getRequest()->getParam('collapse')) { $this->setIsCollapsed(true); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php index 341ee16ae910b..45cd504be201a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php @@ -188,7 +188,7 @@ protected function _processActionData($action = null) && $this->_getOrderCreateModel()->getShippingAddress()->getSameAsBilling() && empty($shippingMethod) ) { $this->_getOrderCreateModel()->setShippingAsBilling(1); - } else { + } elseif ($syncFlag !== null) { $this->_getOrderCreateModel()->setShippingAsBilling((int)$syncFlag); } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index 67a0dc469163b..b4fa6fed6cdf5 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -89,13 +89,18 @@ public function __construct( protected function _prepareShipment($invoice) { $invoiceData = $this->getRequest()->getParam('invoice'); - + $itemArr = []; + if (!isset($invoiceData['items']) || empty($invoiceData['items'])) { + $orderItems = $invoice->getOrder()->getItems(); + foreach ($orderItems as $item) { + $itemArr[$item->getId()] = (int)$item->getQtyOrdered(); + } + } $shipment = $this->shipmentFactory->create( $invoice->getOrder(), - isset($invoiceData['items']) ? $invoiceData['items'] : [], + isset($invoiceData['items']) ? $invoiceData['items'] : $itemArr, $this->getRequest()->getPost('tracking') ); - if (!$shipment->getTotalQty()) { return false; } diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php index a5c7f71df66c5..a3242228b28e0 100644 --- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php +++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php @@ -5,40 +5,35 @@ */ namespace Magento\Sales\Cron; -use Magento\Store\Model\StoresConfig; +use Magento\Quote\Model\ResourceModel\Quote\Collection; +use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection; +use Magento\Store\Model\StoreManagerInterface; /** * Class CleanExpiredQuotes */ class CleanExpiredQuotes { - const LIFETIME = 86400; - - /** - * @var StoresConfig - */ - protected $storesConfig; - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory + * @var ExpiredQuotesCollection */ - protected $quoteCollectionFactory; + private $expiredQuotesCollection; /** - * @var array + * @var StoreManagerInterface */ - protected $expireQuotesFilterFields = []; + private $storeManager; /** - * @param StoresConfig $storesConfig - * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $collectionFactory + * @param StoreManagerInterface $storeManager + * @param ExpiredQuotesCollection $expiredQuotesCollection */ public function __construct( - StoresConfig $storesConfig, - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $collectionFactory + StoreManagerInterface $storeManager, + ExpiredQuotesCollection $expiredQuotesCollection ) { - $this->storesConfig = $storesConfig; - $this->quoteCollectionFactory = $collectionFactory; + $this->storeManager = $storeManager; + $this->expiredQuotesCollection = $expiredQuotesCollection; } /** @@ -48,42 +43,11 @@ public function __construct( */ public function execute() { - $lifetimes = $this->storesConfig->getStoresConfigByPath('checkout/cart/delete_quote_after'); - foreach ($lifetimes as $storeId => $lifetime) { - $lifetime *= self::LIFETIME; - - /** @var $quotes \Magento\Quote\Model\ResourceModel\Quote\Collection */ - $quotes = $this->quoteCollectionFactory->create(); - - $quotes->addFieldToFilter('store_id', $storeId); - $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); - - foreach ($this->getExpireQuotesAdditionalFilterFields() as $field => $condition) { - $quotes->addFieldToFilter($field, $condition); - } - + $stores = $this->storeManager->getStores(true); + foreach ($stores as $store) { + /** @var $quotes Collection */ + $quotes = $this->expiredQuotesCollection->getExpiredQuotes($store); $quotes->walk('delete'); } } - - /** - * Retrieve expire quotes additional fields to filter - * - * @return array - */ - protected function getExpireQuotesAdditionalFilterFields() - { - return $this->expireQuotesFilterFields; - } - - /** - * Set expire quotes additional fields to filter - * - * @param array $fields - * @return void - */ - public function setExpireQuotesAdditionalFilterFields(array $fields) - { - $this->expireQuotesFilterFields = $fields; - } } diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 48deddb2fe5ac..89564f97ccf16 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -5,8 +5,11 @@ */ namespace Magento\Sales\Model; +use Magento\Config\Model\Config\Source\Nooptreq; use Magento\Directory\Model\Currency; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; @@ -14,6 +17,7 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; @@ -24,8 +28,7 @@ use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; use Magento\Sales\Model\ResourceModel\Order\Status\History\Collection as HistoryCollection; -use Magento\Sales\Api\OrderItemRepositoryInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Store\Model\ScopeInterface; /** * Order model @@ -299,6 +302,11 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $searchCriteriaBuilder; + /** + * @var ScopeConfigInterface; + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -331,6 +339,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param ProductOption|null $productOption * @param OrderItemRepositoryInterface $itemRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ScopeConfigInterface $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -364,7 +373,8 @@ public function __construct( ResolverInterface $localeResolver = null, ProductOption $productOption = null, OrderItemRepositoryInterface $itemRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -392,6 +402,7 @@ public function __construct( ->get(OrderItemRepositoryInterface::class); $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() ->get(SearchCriteriaBuilder::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct( $context, @@ -1111,7 +1122,7 @@ public function addStatusHistoryComment($comment, $status = false) { return $this->addCommentToStatusHistory($comment, $status, false); } - + /** * Add a comment to order status history. * @@ -1503,7 +1514,7 @@ public function getItemById($itemId) * Get item by quote item id * * @param mixed $quoteItemId - * @return \Magento\Framework\DataObject|null + * @return \Magento\Framework\DataObject|null */ public function getItemByQuoteItemId($quoteItemId) { @@ -1967,11 +1978,23 @@ public function getRelatedObjects() */ public function getCustomerName() { - if ($this->getCustomerFirstname()) { - $customerName = $this->getCustomerFirstname() . ' ' . $this->getCustomerLastname(); - } else { - $customerName = (string)__('Guest'); + if (null === $this->getCustomerFirstname()) { + return (string)__('Guest'); } + + $customerName = ''; + if ($this->isVisibleCustomerPrefix() && strlen($this->getCustomerPrefix())) { + $customerName .= $this->getCustomerPrefix() . ' '; + } + $customerName .= $this->getCustomerFirstname(); + if ($this->isVisibleCustomerMiddlename() && strlen($this->getCustomerMiddlename())) { + $customerName .= ' ' . $this->getCustomerMiddlename(); + } + $customerName .= ' ' . $this->getCustomerLastname(); + if ($this->isVisibleCustomerSuffix() && strlen($this->getCustomerSuffix())) { + $customerName .= ' ' . $this->getCustomerSuffix(); + } + return $customerName; } @@ -4534,5 +4557,48 @@ public function setShippingMethod($shippingMethod) return $this->setData('shipping_method', $shippingMethod); } + /** + * Is visible customer middlename + * + * @return bool + */ + private function isVisibleCustomerMiddlename(): bool + { + return $this->scopeConfig->isSetFlag( + 'customer/address/middlename_show', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is visible customer prefix + * + * @return bool + */ + private function isVisibleCustomerPrefix(): bool + { + $prefixShowValue = $this->scopeConfig->getValue( + 'customer/address/prefix_show', + ScopeInterface::SCOPE_STORE + ); + + return $prefixShowValue !== Nooptreq::VALUE_NO; + } + + /** + * Is visible customer suffix + * + * @return bool + */ + private function isVisibleCustomerSuffix(): bool + { + $prefixShowValue = $this->scopeConfig->getValue( + 'customer/address/suffix_show', + ScopeInterface::SCOPE_STORE + ); + + return $prefixShowValue !== Nooptreq::VALUE_NO; + } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php index dc920ab62e0b7..f9efb9f56f504 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\CreditmemoCommentIdentity + */ class CreditmemoCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/creditmemo_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/creditmemo_comment/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/creditmemo_comment/identity'; @@ -15,6 +23,8 @@ class CreditmemoCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/creditmemo_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php index f60ef03800cf0..4c1fcfb501e3a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/CreditmemoIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity + */ class CreditmemoIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/creditmemo/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/creditmemo/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/creditmemo/identity'; @@ -15,6 +23,8 @@ class CreditmemoIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/creditmemo/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php index 81584a61b7452..fd0a384a4bc6f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\InvoiceCommentIdentity + */ class InvoiceCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/invoice_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/invoice_comment/copy_to'; const XML_PATH_EMAIL_GUEST_TEMPLATE = 'sales_email/invoice_comment/guest_template'; @@ -15,6 +23,8 @@ class InvoiceCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/invoice_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php index db063b87271fb..6bb4eb0f0fd7f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/InvoiceIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity + */ class InvoiceIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/invoice/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/invoice/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/invoice/identity'; @@ -15,6 +23,8 @@ class InvoiceIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/invoice/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php index 3a79402913cc1..f43cd71ddd39a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/OrderCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\OrderCommentIdentity + */ class OrderCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/order_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/order_comment/copy_to'; const XML_PATH_EMAIL_GUEST_TEMPLATE = 'sales_email/order_comment/guest_template'; @@ -15,6 +23,8 @@ class OrderCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/order_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return email copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php index 68d269b4b4fc8..168869c67fb2d 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/OrderIdentity.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\OrderIdentity + */ class OrderIdentity extends Container implements IdentityInterface { /** @@ -18,6 +23,8 @@ class OrderIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/order/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -38,7 +45,7 @@ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php index 0c2f1d592dd19..db408ceecb4cc 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentCommentIdentity.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\ShipmentCommentIdentity + */ class ShipmentCommentIdentity extends Container implements IdentityInterface { + /** + * Configuration paths + */ const XML_PATH_EMAIL_COPY_METHOD = 'sales_email/shipment_comment/copy_method'; const XML_PATH_EMAIL_COPY_TO = 'sales_email/shipment_comment/copy_to'; const XML_PATH_EMAIL_IDENTITY = 'sales_email/shipment_comment/identity'; @@ -15,6 +23,8 @@ class ShipmentCommentIdentity extends Container implements IdentityInterface const XML_PATH_EMAIL_ENABLED = 'sales_email/shipment_comment/enabled'; /** + * Is email enabled + * * @return bool */ public function isEnabled() @@ -27,18 +37,22 @@ public function isEnabled() } /** + * Return email copy_to list + * * @return array|bool */ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } /** + * Return copy method + * * @return mixed */ public function getCopyMethod() @@ -47,6 +61,8 @@ public function getCopyMethod() } /** + * Return guest template id + * * @return mixed */ public function getGuestTemplateId() @@ -55,6 +71,8 @@ public function getGuestTemplateId() } /** + * Return template id + * * @return mixed */ public function getTemplateId() @@ -63,6 +81,8 @@ public function getTemplateId() } /** + * Return email identity + * * @return mixed */ public function getEmailIdentity() diff --git a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php index ddf682d92fc87..d2f4c03f95b1f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php +++ b/app/code/Magento/Sales/Model/Order/Email/Container/ShipmentIdentity.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Model\Order\Email\Container; +/** + * Class \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity + */ class ShipmentIdentity extends Container implements IdentityInterface { /** @@ -41,7 +45,7 @@ public function getEmailCopyTo() { $data = $this->getConfigValue(self::XML_PATH_EMAIL_COPY_TO, $this->getStore()->getStoreId()); if (!empty($data)) { - return explode(',', $data); + return array_map('trim', explode(',', $data)); } return false; } diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index c4523981ac729..ae188309ea646 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -85,8 +85,8 @@ public function sendCopyTo() $copyTo = $this->identityContainer->getEmailCopyTo(); if (!empty($copyTo)) { - $this->configureEmailTemplate(); foreach ($copyTo as $email) { + $this->configureEmailTemplate(); $this->transportBuilder->addTo($email); $transport = $this->transportBuilder->getTransport(); $transport->sendMessage(); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php new file mode 100644 index 0000000000000..895d73cc4cfff --- /dev/null +++ b/app/code/Magento/Sales/Model/ResourceModel/Collection/ExpiredQuotesCollection.php @@ -0,0 +1,79 @@ +config = $config; + $this->quoteCollectionFactory = $collectionFactory; + } + + /** + * Gets expired quotes + * + * Quote is considered expired if the latest update date + * of the quote is greater than lifetime threshold + * + * @param StoreInterface $store + * @return AbstractCollection + */ + public function getExpiredQuotes(StoreInterface $store): AbstractCollection + { + $lifetime = $this->config->getValue( + $this->quoteLifetime, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $lifetime *= $this->secondsInDay; + + /** @var $quotes Collection */ + $quotes = $this->quoteCollectionFactory->create(); + $quotes->addFieldToFilter('store_id', $store->getId()); + $quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]); + + return $quotes; + } +} diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml new file mode 100644 index 0000000000000..4fd992418887a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + Assert product in Shopping cart section in Customer's Activities block on Create Order Page. + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..8512387d45e8a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + Move product to the "Items Ordered" section from shopping cart. + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 3f178ae02102a..90e2aa8e12527 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -92,7 +92,7 @@ Clears the Email, First Name, Last Name, Street Line 1, City, Postal Code and Phone fields when adding an Order and then verifies that they are required after attempting to Save. - + @@ -181,7 +181,7 @@ EXTENDS: addConfigurableProductToOrder. Selects the provided Option to the Configurable Product. - + @@ -195,7 +195,7 @@ - + @@ -213,7 +213,7 @@ - + @@ -235,7 +235,7 @@ - + {{price}} @@ -320,6 +320,22 @@ + + + Change Shipping Method on the Admin 'Create New Order for' page. + + + + + + + + + + + + + @@ -516,7 +532,7 @@ - + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..b8493bf288378 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderSelectShippingMethodActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Select Shipping method from admin order page. + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml index aaeb9ffb30bd9..7388eaa96f215 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml @@ -15,7 +15,7 @@ - + diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml new file mode 100644 index 0000000000000..03a14ca514091 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + Navigate to customer dashboard -> orders. Press 'reorder' button for specified order id. Notice: customer should be logged in. + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml index fda886a839802..32e987bea919b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml @@ -17,5 +17,7 @@ + + - \ No newline at end of file + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml index 71a96dc109385..653b1d48686e3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInformationSection.xml @@ -13,6 +13,8 @@ + + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml index 2d1a4d5a4cbae..4fde9db1d21d8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml @@ -9,6 +9,7 @@
+ @@ -38,4 +39,4 @@
-
\ No newline at end of file + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index b31582552cccc..a478d79d8553f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -9,6 +9,7 @@
+ diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml index 6f62ce199ecbb..f57b1f65eb94e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml @@ -11,6 +11,6 @@
- +
- \ No newline at end of file + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index ace64cdaa1032..a18ca0c415567 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -39,5 +39,6 @@ +
diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml index 415bac7fd051d..c0deb9ab55d2b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml index 1ad736ade37fc..4310d412d1c98 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -55,6 +55,9 @@ + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index 800517236cb39..a90fe5c49f032 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -25,7 +25,7 @@ - + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml new file mode 100644 index 0000000000000..bd13f7c847c34 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml @@ -0,0 +1,50 @@ + + + + + + + + <stories value="Create order and add product using checkbox"/> + <description value="Create order in Admin panel, add product by clicking checkbox, and verify it is checked"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <!-- Create simple customer --> + <createData entity="Simple_US_Customer_CA" stepKey="createSimpleCustomer"/> + + <!-- Create simple product --> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + + <!-- Login to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Initiate create new order --> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createSimpleCustomer$$"/> + </actionGroup> + + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSkuFilterBundle"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml new file mode 100644 index 0000000000000..d6bf0eec301db --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest"> + <annotations> + <title value="Tax should not be displayed for non taxable address"/> + <stories value="MC-21699: Tax does not change when changing the billing address from Admin Panel"/> + <description value="Tax should not be displayed for non taxable address when switching from taxable address"/> + <testCaseId value="MC-21721"/> + <features value="Sales"/> + <severity value="MAJOR"/> + <group value="Sales"/> + </annotations> + <before> + <!--Enable flat rate shipping--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!--Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <!--Create customer--> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="simpleCustomer"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="category1"/> + <!--Create product1--> + <createData entity="_defaultProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + <!--Create tax rule for US-CA--> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <!--Step 1: Create new order for customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <!--Step 2: Add product1 to the order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <!--Step 2: Select taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_CA.state}}" stepKey="selectTaxableAddress" /> + <!--Step 3: Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShippingMethod"/> + <!--Step 4: Verify that tax is applied to the order--> + <seeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="seeTax" /> + <!--Step 5: Select non taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_TX.state}}" stepKey="selectNonTaxableAddress" /> + <!--Step 6: Change shipping method to Free--> + <actionGroup ref="changeShippingMethod" stepKey="changeShippingMethod"> + <argument name="shippingMethod" value="freeshipping_freeshipping"/> + </actionGroup> + <!--Step 7: Verify that tax is not applied to the order--> + <dontSeeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="dontSeeTax" /> + <after> + <!--Delete product1--> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <!--Delete category--> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <!--Delete customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <!--Delete tax rule--> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + <!--Disable free shipping method --> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml new file mode 100644 index 0000000000000..b40e9d041a10e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest"> + <annotations> + <stories value="Admin create order"/> + <title value="Product in the shopping cart could be reached by admin during order creation with multi website config"/> + <description value="Product in the shopping cart could be reached by admin during order creation with multi website config"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6353"/> + <group value="sales"/> + <skip> + <issueId value="MC-20129"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="goToProductPageViaID" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createProduct.id$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create customer account for Second Website--> + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageUsingStoreCodeInUrlActionGroup" stepKey="goToCreateCustomerPage"/> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Thank you for registering with {{customStoreGroup.name}}." /> + </actionGroup> + + <!--Open product page and add to cart--> + <actionGroup ref="StorefrontOpenProductPageUsingStoreCodeInUrlActionGroup" stepKey="openProductPageUsingStoreCodeInUrl"> + <argument name="product" value="$$createProduct$$"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="addProductToCart"/> + + <!--Create new order for existing Customer And Store--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="createNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!--Assert product in Shopping cart section--> + <actionGroup ref="AdminAssertProductInShoppingCartSectionActionGroup" stepKey="seeProductInShoppingCart"> + <argument name="product" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Move product to the order from shopping cart--> + <actionGroup ref="AdminMoveProductToItemsOrderedFromShoppingCartActionGroup" stepKey="addProductToItemsOrderedFromShoppingCart"> + <argument name="product" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!--Select shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + + <!--Checkout select Check/Money Order payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + + <!--Submit Order and verify information--> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index e487c62b96727..255a7a91f9b10 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -29,7 +29,7 @@ <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 0" /> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index ed536bd3351f9..01021ad745f70 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 1490fc1a1a388..9268e9e728658 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -25,7 +25,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!--Create order via Admin--> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 93f4233af90e4..ec0f97e418c8c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -33,7 +33,7 @@ <argument name="ruleName" value="{{ApiSalesRule.name}}"/> </actionGroup> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct" stepKey="deleteCategory1"/> </after> @@ -58,7 +58,7 @@ <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <!-- Place an order from Storefront as a Guest --> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php index 94148cc515382..2b08daf02134e 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php @@ -95,6 +95,11 @@ protected function setUp() '_orderCreate' => $this->orderCreate ] ); + + // Do not display VAT validation button on edit order address form + // Emulate fix done in controller + /** @see \Magento\Sales\Controller\Adminhtml\Order\Address::execute */ + $this->addressBlock->setDisplayVatValidationButton(false); } public function testGetForm() diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php deleted file mode 100644 index ad6a3e03ba679..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php +++ /dev/null @@ -1,84 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Cron; - -use \Magento\Sales\Cron\CleanExpiredQuotes; - -/** - * Tests Magento\Sales\Cron\CleanExpiredQuotes - */ -class CleanExpiredQuotesTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Store\Model\StoresConfig|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storesConfigMock; - - /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteFactoryMock; - - /** - * @var \Magento\Sales\Cron\CleanExpiredQuotes - */ - protected $observer; - - protected function setUp() - { - $this->storesConfigMock = $this->createMock(\Magento\Store\Model\StoresConfig::class); - - $this->quoteFactoryMock = $this->getMockBuilder( - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->observer = new CleanExpiredQuotes($this->storesConfigMock, $this->quoteFactoryMock); - } - - /** - * @param array $lifetimes - * @param array $additionalFilterFields - * @dataProvider cleanExpiredQuotesDataProvider - */ - public function testExecute($lifetimes, $additionalFilterFields) - { - $this->storesConfigMock->expects($this->once()) - ->method('getStoresConfigByPath') - ->with($this->equalTo('checkout/cart/delete_quote_after')) - ->will($this->returnValue($lifetimes)); - - $quotesMock = $this->getMockBuilder(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->quoteFactoryMock->expects($this->exactly(count($lifetimes))) - ->method('create') - ->will($this->returnValue($quotesMock)); - $quotesMock->expects($this->exactly((2 + count($additionalFilterFields)) * count($lifetimes))) - ->method('addFieldToFilter'); - if (!empty($lifetimes)) { - $quotesMock->expects($this->exactly(count($lifetimes))) - ->method('walk') - ->with('delete'); - } - $this->observer->setExpireQuotesAdditionalFilterFields($additionalFilterFields); - $this->observer->execute(); - } - - /** - * @return array - */ - public function cleanExpiredQuotesDataProvider() - { - return [ - [[], []], - [[1 => 100, 2 => 200], []], - [[1 => 100, 2 => 200], ['field1' => 'condition1', 'field2' => 'condition2']], - ]; - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php index 68e1c7c17cd1c..d255f88dea359 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(CreditmemoCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php index 3da6dfe78eb40..1e3ff11ea73c1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/CreditmemoIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(CreditmemoIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php index b7ec911212254..5eeaa12a736f6 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(InvoiceCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php index 4a63541bc05f2..3328d2f35b2b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/InvoiceIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(InvoiceIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php index 1dc53b97711ab..0892ba34114be 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(OrderCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php index f554c2aeef168..54d1ab872fb1d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/OrderIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(OrderIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php index a8d676be2a2f1..ff62b46e0cac9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentCommentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(ShipmentCommentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php index 503646a5eac61..bccf109783913 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Container/ShipmentIdentityTest.php @@ -78,6 +78,21 @@ public function testGetEmailCopyTo() $this->assertEquals(['test_value', 'test_value2'], $result); } + public function testGetEmailCopyToWithSpaceEmail() + { + $this->scopeConfigInterfaceMock->expects($this->once()) + ->method('getValue') + ->with( + $this->equalTo(ShipmentIdentity::XML_PATH_EMAIL_COPY_TO), + $this->equalTo(\Magento\Store\Model\ScopeInterface::SCOPE_STORE), + $this->equalTo($this->storeId) + ) + ->will($this->returnValue('test_value, test_value2')); + $this->identity->setStore($this->storeMock); + $result = $this->identity->getEmailCopyTo(); + $this->assertEquals(['test_value', 'test_value2'], $result); + } + public function testGetEmailCopyToEmptyResult() { $this->scopeConfigInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index adfb697e70331..756048d287e46 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -37,11 +37,6 @@ class SenderBuilderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $templateId = 'test_template_id'; - $templateOptions = ['option1', 'option2']; - $templateVars = ['var1', 'var2']; - $emailIdentity = 'email_identity_test'; - $emailCopyTo = ['example@mail.com']; $this->templateContainerMock = $this->createPartialMock( \Magento\Sales\Model\Order\Email\Container\Template::class, @@ -83,36 +78,6 @@ protected function setUp() ] ); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateId') - ->will($this->returnValue($templateId)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($this->equalTo($templateId)); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateOptions') - ->will($this->returnValue($templateOptions)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with($this->equalTo($templateOptions)); - $this->templateContainerMock->expects($this->once()) - ->method('getTemplateVars') - ->will($this->returnValue($templateVars)); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with($this->equalTo($templateVars)); - - $this->identityContainerMock->expects($this->once()) - ->method('getEmailIdentity') - ->will($this->returnValue($emailIdentity)); - $this->transportBuilder->expects($this->once()) - ->method('setFromByScope') - ->with($this->equalTo($emailIdentity), 1); - - $this->identityContainerMock->expects($this->once()) - ->method('getEmailCopyTo') - ->will($this->returnValue($emailCopyTo)); - $this->senderBuilder = new SenderBuilder( $this->templateContainerMock, $this->identityContainerMock, @@ -122,6 +87,7 @@ protected function setUp() public function testSend() { + $this->setExpectedCount(1); $customerName = 'test_name'; $customerEmail = 'test_email'; $identity = 'email_identity_test'; @@ -142,20 +108,20 @@ public function testSend() $this->identityContainerMock->expects($this->once()) ->method('getCustomerName') ->will($this->returnValue($customerName)); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(1)) ->method('getStore') ->willReturn($this->storeMock); $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('setFromByScope') ->with($identity, 1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('addTo') ->with($this->equalTo($customerEmail), $this->equalTo($customerName)); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(1)) ->method('getTransport') ->will($this->returnValue($transportMock)); @@ -164,6 +130,7 @@ public function testSend() public function testSendCopyTo() { + $this->setExpectedCount(2); $identity = 'email_identity_test'; $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class @@ -172,22 +139,66 @@ public function testSendCopyTo() ->method('getCustomerEmail'); $this->identityContainerMock->expects($this->never()) ->method('getCustomerName'); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($this->equalTo('example@mail.com')); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(2)) + ->method('addTo'); + $this->transportBuilder->expects($this->exactly(2)) ->method('setFromByScope') ->with($identity, 1); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('getStore') ->willReturn($this->storeMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getId') ->willReturn(1); - $this->transportBuilder->expects($this->once()) + $this->transportBuilder->expects($this->exactly(2)) ->method('getTransport') ->will($this->returnValue($transportMock)); $this->senderBuilder->sendCopyTo(); } + + /** + * Sets expected count invocation. + * + * @param int $count + */ + private function setExpectedCount(int $count = 1) + { + + $templateId = 'test_template_id'; + $templateOptions = ['option1', 'option2']; + $templateVars = ['var1', 'var2']; + $emailIdentity = 'email_identity_test'; + $emailCopyTo = ['example@mail.com', 'example2@mail.com']; + + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateId') + ->will($this->returnValue($templateId)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateIdentifier') + ->with($this->equalTo($templateId)); + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateOptions') + ->will($this->returnValue($templateOptions)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateOptions') + ->with($this->equalTo($templateOptions)); + $this->templateContainerMock->expects($this->exactly($count)) + ->method('getTemplateVars') + ->will($this->returnValue($templateVars)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setTemplateVars') + ->with($this->equalTo($templateVars)); + + $this->identityContainerMock->expects($this->exactly($count)) + ->method('getEmailIdentity') + ->will($this->returnValue($emailIdentity)); + $this->transportBuilder->expects($this->exactly($count)) + ->method('setFromByScope') + ->with($this->equalTo($emailIdentity), 1); + + $this->identityContainerMock->expects($this->once()) + ->method('getEmailCopyTo') + ->will($this->returnValue($emailCopyTo)); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 705d2face2308..bd6487caff7dd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model; use Magento\Catalog\Api\Data\ProductInterface; @@ -17,6 +18,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteria; use Magento\Sales\Api\Data\OrderItemSearchResultInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; /** * Test class for \Magento\Sales\Model\Order @@ -24,6 +27,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.TooManyFields) */ class OrderTest extends \PHPUnit\Framework\TestCase { @@ -102,6 +106,11 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ private $searchCriteriaBuilder; + /** + * @var MockObject|ScopeConfigInterface $scopeConfigMock + */ + private $scopeConfigMock; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -125,14 +134,17 @@ protected function setUp() \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class, ['create'] ); - $this->item = $this->createPartialMock(\Magento\Sales\Model\ResourceModel\Order\Item::class, [ + $this->item = $this->createPartialMock( + \Magento\Sales\Model\ResourceModel\Order\Item::class, + [ 'isDeleted', 'getQtyToInvoice', 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice', 'getProductId', - ]); + ] + ); $this->salesOrderCollectionMock = $this->getMockBuilder( \Magento\Sales\Model\ResourceModel\Order\Collection::class )->disableOriginalConstructor() @@ -168,6 +180,7 @@ protected function setUp() ->setMethods(['addFilter', 'create']) ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $this->order = $helper->getObject( \Magento\Sales\Model\Order::class, [ @@ -182,7 +195,8 @@ protected function setUp() 'localeResolver' => $this->localeResolver, 'timezone' => $this->timezone, 'itemRepository' => $this->itemRepository, - 'searchCriteriaBuilder' => $this->searchCriteriaBuilder + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'scopeConfig' => $this->scopeConfigMock ] ); } @@ -354,6 +368,51 @@ public function testCanInvoice() $this->assertTrue($this->order->canInvoice()); } + /** + * Ensure customer name returned correctly. + * + * @dataProvider customerNameProvider + * @param array $expectedData + */ + public function testGetCustomerName(array $expectedData) + { + $this->order->setCustomerFirstname($expectedData['first_name']); + $this->order->setCustomerSuffix($expectedData['customer_suffix']); + $this->order->setCustomerPrefix($expectedData['customer_prefix']); + $this->scopeConfigMock->expects($this->exactly($expectedData['invocation'])) + ->method('isSetFlag') + ->willReturn(true); + $this->assertEquals($expectedData['expected_name'], $this->order->getCustomerName()); + } + + /** + * Customer name data provider + */ + public function customerNameProvider() + { + return + [ + [ + [ + 'first_name' => null, + 'invocation' => 0, + 'expected_name' => 'Guest', + 'customer_suffix' => 'smith', + 'customer_prefix' => 'mr.' + ] + ], + [ + [ + 'first_name' => 'Smith', + 'invocation' => 1, + 'expected_name' => 'mr. Smith Carl', + 'customer_suffix' => 'Carl', + 'customer_prefix' => 'mr.' + ] + ] + ]; + } + /** * @param string $status * @@ -819,9 +878,10 @@ public function testCanVoidPayment($actionFlags, $orderState) if ($orderState == \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW) { $canVoidOrder = false; } - if ($orderState == \Magento\Sales\Model\Order::STATE_HOLDED && (!isset( - $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] - ) || $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] !== false) + if ($orderState == \Magento\Sales\Model\Order::STATE_HOLDED && + (!isset($actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD]) || + $actionFlags[\Magento\Sales\Model\Order::ACTION_FLAG_UNHOLD] !== false + ) ) { $canVoidOrder = false; } @@ -1193,6 +1253,9 @@ public function testGetCreatedAtFormattedUsesCorrectLocale() $this->order->getCreatedAtFormatted(\IntlDateFormatter::SHORT); } + /** + * @return array + */ public function notInvoicingStatesProvider() { return [ @@ -1202,6 +1265,9 @@ public function notInvoicingStatesProvider() ]; } + /** + * @return array + */ public function canNotCreditMemoStatesProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index 99a411c43c247..30513571fb71d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -103,6 +103,9 @@ public function testCheck( $this->assertEquals($expectedState, $this->orderMock->getState()); } + /** + * @return array + */ public function stateCheckDataProvider() { return [ diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 1f781604491bf..eb508af8daf25 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sales_order" resource="sales" engine="innodb" comment="Sales Flat Order"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="state" nullable="true" length="32" comment="State"/> <column xsi:type="varchar" name="status" nullable="true" length="32" comment="Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="255" comment="Coupon Code"/> @@ -19,9 +19,9 @@ <column xsi:type="smallint" name="is_virtual" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Virtual"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="20" unsigned="false" @@ -145,7 +145,7 @@ <column xsi:type="smallint" name="customer_note_notify" padding="5" unsigned="true" nullable="true" identity="false" comment="Customer Note Notify"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="true" identity="false"/> <column xsi:type="int" name="edit_increment" padding="11" unsigned="false" nullable="true" identity="false" comment="Edit Increment"/> @@ -158,11 +158,11 @@ <column xsi:type="int" name="payment_auth_expiration" padding="11" unsigned="false" nullable="true" identity="false" comment="Payment Authorization Expiration"/> <column xsi:type="int" name="quote_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Address Id"/> + comment="Quote Address ID"/> <column xsi:type="int" name="quote_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Id"/> + comment="Quote ID"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" @@ -188,7 +188,7 @@ <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> <column xsi:type="datetime" name="customer_dob" on_update="false" nullable="true" comment="Customer Dob"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="32" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="32" comment="Increment ID"/> <column xsi:type="varchar" name="applied_rule_ids" nullable="true" length="128" comment="Applied Rule Ids"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="128" comment="Customer Email"/> @@ -201,21 +201,21 @@ <column xsi:type="varchar" name="customer_taxvat" nullable="true" length="32" comment="Customer Taxvat"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> - <column xsi:type="varchar" name="ext_customer_id" nullable="true" length="32" comment="Ext Customer Id"/> - <column xsi:type="varchar" name="ext_order_id" nullable="true" length="32" comment="Ext Order Id"/> + <column xsi:type="varchar" name="ext_customer_id" nullable="true" length="32" comment="Ext Customer ID"/> + <column xsi:type="varchar" name="ext_order_id" nullable="true" length="32" comment="Ext Order ID"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> <column xsi:type="varchar" name="hold_before_state" nullable="true" length="32" comment="Hold Before State"/> <column xsi:type="varchar" name="hold_before_status" nullable="true" length="32" comment="Hold Before Status"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="original_increment_id" nullable="true" length="32" - comment="Original Increment Id"/> - <column xsi:type="varchar" name="relation_child_id" nullable="true" length="32" comment="Relation Child Id"/> + comment="Original Increment ID"/> + <column xsi:type="varchar" name="relation_child_id" nullable="true" length="32" comment="Relation Child ID"/> <column xsi:type="varchar" name="relation_child_real_id" nullable="true" length="32" - comment="Relation Child Real Id"/> - <column xsi:type="varchar" name="relation_parent_id" nullable="true" length="32" comment="Relation Parent Id"/> + comment="Relation Child Real ID"/> + <column xsi:type="varchar" name="relation_parent_id" nullable="true" length="32" comment="Relation Parent ID"/> <column xsi:type="varchar" name="relation_parent_real_id" nullable="true" length="32" - comment="Relation Parent Real Id"/> + comment="Relation Parent Real ID"/> <column xsi:type="varchar" name="remote_ip" nullable="true" length="45" comment="Remote Ip"/> <column xsi:type="varchar" name="shipping_method" nullable="true" length="120"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> @@ -297,13 +297,13 @@ </table> <table name="sales_order_grid" resource="sales" engine="innodb" comment="Sales Flat Order Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="status" nullable="true" length="32" comment="Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" @@ -312,7 +312,7 @@ comment="Grand Total"/> <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="255" comment="Order Currency Code"/> @@ -386,17 +386,17 @@ </table> <table name="sales_order_address" resource="sales" engine="innodb" comment="Sales Flat Order Address"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="customer_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Address Id"/> + comment="Customer Address ID"/> <column xsi:type="int" name="quote_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Address Id"/> + comment="Quote Address ID"/> <column xsi:type="int" name="region_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Region Id"/> + comment="Region ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="fax" nullable="true" length="255" comment="Fax"/> <column xsi:type="varchar" name="region" nullable="true" length="255" comment="Region"/> <column xsi:type="varchar" name="postcode" nullable="true" length="255" comment="Postcode"/> @@ -405,17 +405,17 @@ <column xsi:type="varchar" name="city" nullable="true" length="255" comment="City"/> <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="varchar" name="telephone" nullable="true" length="255" comment="Phone Number"/> - <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country Id"/> + <column xsi:type="varchar" name="country_id" nullable="true" length="2" comment="Country ID"/> <column xsi:type="varchar" name="firstname" nullable="true" length="255" comment="Firstname"/> <column xsi:type="varchar" name="address_type" nullable="true" length="255" comment="Address Type"/> <column xsi:type="varchar" name="prefix" nullable="true" length="255" comment="Prefix"/> <column xsi:type="varchar" name="middlename" nullable="true" length="255" comment="Middlename"/> <column xsi:type="varchar" name="suffix" nullable="true" length="255" comment="Suffix"/> <column xsi:type="varchar" name="company" nullable="true" length="255" comment="Company"/> - <column xsi:type="text" name="vat_id" nullable="true" comment="Vat Id"/> + <column xsi:type="text" name="vat_id" nullable="true" comment="Vat ID"/> <column xsi:type="smallint" name="vat_is_valid" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Is Valid"/> - <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request Id"/> + <column xsi:type="text" name="vat_request_id" nullable="true" comment="Vat Request ID"/> <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> @@ -431,9 +431,9 @@ </table> <table name="sales_order_status_history" resource="sales" engine="innodb" comment="Sales Flat Order Status History"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -459,21 +459,21 @@ </table> <table name="sales_order_item" resource="sales" engine="innodb" comment="Sales Flat Order Item"> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Order Id"/> + default="0" comment="Order ID"/> <column xsi:type="int" name="parent_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Item Id"/> + comment="Parent Item ID"/> <column xsi:type="int" name="quote_item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Quote Item Id"/> + comment="Quote Item ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_type" nullable="true" length="255" comment="Product Type"/> <column xsi:type="text" name="product_options" nullable="true" comment="Product Options"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" @@ -549,7 +549,7 @@ nullable="true" comment="Base Tax Before Discount"/> <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> - <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item Id"/> + <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item ID"/> <column xsi:type="smallint" name="locked_do_invoice" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Invoice"/> <column xsi:type="smallint" name="locked_do_ship" padding="5" unsigned="true" nullable="true" identity="false" @@ -602,9 +602,9 @@ </table> <table name="sales_order_payment" resource="sales" engine="innodb" comment="Sales Flat Order Payment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Captured"/> <column xsi:type="decimal" name="shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" @@ -642,7 +642,7 @@ <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Canceled"/> <column xsi:type="int" name="quote_payment_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Quote Payment Id"/> + comment="Quote Payment ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="varchar" name="cc_exp_month" nullable="true" length="12" comment="Cc Exp Month"/> <column xsi:type="varchar" name="cc_ss_start_year" nullable="true" length="12" comment="Cc Ss Start Year"/> @@ -663,7 +663,7 @@ <column xsi:type="varchar" name="cc_ss_start_month" nullable="true" length="128" comment="Cc Ss Start Month"/> <column xsi:type="varchar" name="echeck_account_type" nullable="true" length="255" comment="Echeck Account Type"/> - <column xsi:type="varchar" name="last_trans_id" nullable="true" length="255" comment="Last Trans Id"/> + <column xsi:type="varchar" name="last_trans_id" nullable="true" length="255" comment="Last Trans ID"/> <column xsi:type="varchar" name="cc_cid_status" nullable="true" length="32" comment="Cc Cid Status"/> <column xsi:type="varchar" name="cc_owner" nullable="true" length="128" comment="Cc Owner"/> <column xsi:type="varchar" name="cc_type" nullable="true" length="32" comment="Cc Type"/> @@ -681,7 +681,7 @@ comment="Echeck Account Name"/> <column xsi:type="varchar" name="cc_avs_status" nullable="true" length="32" comment="Cc Avs Status"/> <column xsi:type="varchar" name="cc_number_enc" nullable="true" length="128"/> - <column xsi:type="varchar" name="cc_trans_id" nullable="true" length="32" comment="Cc Trans Id"/> + <column xsi:type="varchar" name="cc_trans_id" nullable="true" length="32" comment="Cc Trans ID"/> <column xsi:type="varchar" name="address_status" nullable="true" length="32" comment="Address Status"/> <column xsi:type="text" name="additional_information" nullable="true" comment="Additional Information"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -696,9 +696,9 @@ </table> <table name="sales_shipment" resource="sales" engine="innodb" comment="Sales Flat Shipment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="total_weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Weight"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" @@ -708,16 +708,16 @@ <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" comment="Send Email"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="shipment_status" padding="11" unsigned="false" nullable="true" identity="false" comment="Shipment Status"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -762,15 +762,15 @@ </table> <table name="sales_shipment_grid" resource="sales" engine="innodb" comment="Sales Flat Shipment Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment Id"/> + comment="Store ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="false" - default="CURRENT_TIMESTAMP" comment="Order Increment Id"/> + default="CURRENT_TIMESTAMP" comment="Order Increment ID"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty"/> @@ -837,9 +837,9 @@ </table> <table name="sales_shipment_item" resource="sales" engine="innodb" comment="Sales Flat Shipment Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="true" @@ -848,9 +848,9 @@ comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> @@ -867,14 +867,14 @@ </table> <table name="sales_shipment_track" resource="sales" engine="innodb" comment="Sales Flat Shipment Track"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Qty"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="text" name="track_number" nullable="true" comment="Number"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> @@ -901,9 +901,9 @@ </table> <table name="sales_shipment_comment" resource="sales" engine="innodb" comment="Sales Flat Shipment Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -926,9 +926,9 @@ </table> <table name="sales_invoice" resource="sales" engine="innodb" comment="Sales Flat Invoice"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" @@ -968,11 +968,11 @@ <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="smallint" name="is_used_for_refund" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Used For Refund"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="smallint" name="email_sent" padding="5" unsigned="true" nullable="true" identity="false" comment="Email Sent"/> <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" @@ -982,14 +982,14 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> - <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction Id"/> + <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction ID"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -1051,16 +1051,16 @@ </table> <table name="sales_invoice_grid" resource="sales" engine="innodb" comment="Sales Flat Invoice Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment Id"/> + comment="Order ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="true" comment="Order Created At"/> <column xsi:type="varchar" name="customer_name" nullable="true" length="255" comment="Customer Name"/> @@ -1136,9 +1136,9 @@ </table> <table name="sales_invoice_item" resource="sales" engine="innodb" comment="Sales Flat Invoice Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" @@ -1167,9 +1167,9 @@ <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> @@ -1192,9 +1192,9 @@ </table> <table name="sales_invoice_comment" resource="sales" engine="innodb" comment="Sales Flat Invoice Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="smallint" name="is_customer_notified" padding="5" unsigned="true" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -1217,9 +1217,9 @@ </table> <table name="sales_creditmemo" resource="sales" engine="innodb" comment="Sales Flat Creditmemo"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" @@ -1269,7 +1269,7 @@ <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="smallint" name="email_sent" padding="5" unsigned="true" nullable="true" identity="false" comment="Email Sent"/> <column xsi:type="smallint" name="send_email" padding="5" unsigned="true" nullable="true" identity="false" @@ -1279,18 +1279,18 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="State"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Shipping Address Id"/> + comment="Shipping Address ID"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Billing Address Id"/> + comment="Billing Address ID"/> <column xsi:type="int" name="invoice_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Invoice Id"/> + comment="Invoice ID"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> <column xsi:type="varchar" name="order_currency_code" nullable="true" length="3" comment="Order Currency Code"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="3" comment="Global Currency Code"/> - <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + <column xsi:type="varchar" name="transaction_id" nullable="true" length="255" comment="Transaction ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -1350,13 +1350,13 @@ </table> <table name="sales_creditmemo_grid" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Grid"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> - <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> + comment="Entity ID"/> + <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> - <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment Id"/> + comment="Order ID"/> + <column xsi:type="varchar" name="order_increment_id" nullable="true" length="50" comment="Order Increment ID"/> <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="true" comment="Order Created At"/> <column xsi:type="varchar" name="billing_name" nullable="true" length="255" comment="Billing Name"/> @@ -1366,13 +1366,13 @@ comment="Base Grand Total"/> <column xsi:type="varchar" name="order_status" nullable="true" length="32" comment="Order Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="billing_address" nullable="true" length="255" comment="Billing Address"/> <column xsi:type="varchar" name="shipping_address" nullable="true" length="255" comment="Shipping Address"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="128" comment="Customer Email"/> <column xsi:type="smallint" name="customer_group_id" padding="6" unsigned="false" nullable="true" - identity="false" comment="Customer Group Id"/> + identity="false" comment="Customer Group ID"/> <column xsi:type="varchar" name="payment_method" nullable="true" length="32" comment="Payment Method"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> @@ -1440,9 +1440,9 @@ </table> <table name="sales_creditmemo_item" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Item"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" @@ -1471,9 +1471,9 @@ <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> <column xsi:type="int" name="product_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="int" name="order_item_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Order Item Id"/> + comment="Order Item ID"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> @@ -1496,9 +1496,9 @@ </table> <table name="sales_creditmemo_comment" resource="sales" engine="innodb" comment="Sales Flat Creditmemo Comment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="is_customer_notified" padding="11" unsigned="false" nullable="true" identity="false" comment="Is Customer Notified"/> <column xsi:type="smallint" name="is_visible_on_front" padding="5" unsigned="true" nullable="false" @@ -1520,10 +1520,10 @@ </index> </table> <table name="sales_invoiced_aggregated" resource="sales" engine="innodb" comment="Sales Invoiced Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1552,10 +1552,10 @@ </table> <table name="sales_invoiced_aggregated_order" resource="sales" engine="innodb" comment="Sales Invoiced Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1584,10 +1584,10 @@ </table> <table name="sales_order_aggregated_created" resource="sales" engine="innodb" comment="Sales Order Aggregated Created"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1638,10 +1638,10 @@ </table> <table name="sales_order_aggregated_updated" resource="sales" engine="innodb" comment="Sales Order Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1692,15 +1692,15 @@ </table> <table name="sales_payment_transaction" resource="sales" engine="innodb" comment="Sales Payment Transaction"> <column xsi:type="int" name="transaction_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Transaction Id"/> + comment="Transaction ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Order Id"/> + default="0" comment="Order ID"/> <column xsi:type="int" name="payment_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Payment Id"/> - <column xsi:type="varchar" name="txn_id" nullable="true" length="100" comment="Txn Id"/> - <column xsi:type="varchar" name="parent_txn_id" nullable="true" length="100" comment="Parent Txn Id"/> + default="0" comment="Payment ID"/> + <column xsi:type="varchar" name="txn_id" nullable="true" length="100" comment="Txn ID"/> + <column xsi:type="varchar" name="parent_txn_id" nullable="true" length="100" comment="Parent Txn ID"/> <column xsi:type="varchar" name="txn_type" nullable="true" length="15" comment="Txn Type"/> <column xsi:type="smallint" name="is_closed" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Is Closed"/> @@ -1732,10 +1732,10 @@ </index> </table> <table name="sales_refunded_aggregated" resource="sales" engine="innodb" comment="Sales Refunded Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1762,10 +1762,10 @@ </table> <table name="sales_refunded_aggregated_order" resource="sales" engine="innodb" comment="Sales Refunded Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> @@ -1791,10 +1791,10 @@ </index> </table> <table name="sales_shipping_aggregated" resource="sales" engine="innodb" comment="Sales Shipping Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="shipping_description" nullable="true" length="255" comment="Shipping Description"/> @@ -1822,10 +1822,10 @@ </table> <table name="sales_shipping_aggregated_order" resource="sales" engine="innodb" comment="Sales Shipping Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="shipping_description" nullable="true" length="255" comment="Shipping Description"/> @@ -1853,12 +1853,12 @@ </table> <table name="sales_bestsellers_aggregated_daily" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Daily"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1886,12 +1886,12 @@ </table> <table name="sales_bestsellers_aggregated_monthly" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Monthly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1919,12 +1919,12 @@ </table> <table name="sales_bestsellers_aggregated_yearly" resource="sales" engine="innodb" comment="Sales Bestsellers Aggregated Yearly"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product Name"/> <column xsi:type="decimal" name="product_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Product Price"/> @@ -1952,9 +1952,9 @@ </table> <table name="sales_order_tax" resource="sales" engine="innodb" comment="Sales Order Tax Table"> <column xsi:type="int" name="tax_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Tax Id"/> + comment="Tax ID"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order Id"/> + comment="Order ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="decimal" name="percent" scale="4" precision="12" unsigned="false" nullable="true" @@ -1982,11 +1982,11 @@ </table> <table name="sales_order_tax_item" resource="sales" engine="innodb" comment="Sales Order Tax Item"> <column xsi:type="int" name="tax_item_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Tax Item Id"/> + comment="Tax Item ID"/> <column xsi:type="int" name="tax_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Tax Id"/> + comment="Tax ID"/> <column xsi:type="int" name="item_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Item Id"/> + comment="Item ID"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="false" comment="Real Tax Percent For Item"/> <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="false" @@ -2046,7 +2046,7 @@ <table name="sales_order_status_label" resource="sales" engine="innodb" comment="Sales Order Status Label Table"> <column xsi:type="varchar" name="status" nullable="false" length="32" comment="Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="false" length="128" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status"/> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml index b700f1b3a65ad..70373f177d8be 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml @@ -81,8 +81,8 @@ $_order = $block->getOrder() ?> </tr> <?php endif; ?> <tr bgcolor="#DEE5E8"> - <td colspan="2" align="right" style="padding:3px 9px"><strong><big><?= $block->escapeHtml(__('Grand Total')) ?></big></strong></td> - <td align="right" style="padding:6px 9px"><strong><big><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></big></strong></td> + <td colspan="2" align="right" style="padding:3px 9px"><strong style="font-size: larger"><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> + <td align="right" style="padding:6px 9px"><strong style="font-size: larger"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></strong></td> </tr> </tfoot> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml index 87d7c85c2d9ed..f8e914a2c9b2f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml @@ -6,7 +6,7 @@ ?> <?php if ($block->getCanDisplayTotalDue()) : ?> <tr> - <td class="label"><big><strong><?= $block->escapeHtml(__('Total Due')) ?></strong></big></td> - <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></big></td> + <td class="label"><strong style="font-size: larger"><?= $block->escapeHtml(__('Total Due')) ?></strong></td> + <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml index dc76799251c7a..af5d58d47fce1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml @@ -9,13 +9,13 @@ <tr> <td class="label"> - <strong><big> + <strong style="font-size: larger"> <?php if ($block->getGrandTotalTitle()) : ?> <?= $block->escapeHtml($block->getGrandTotalTitle()) ?> <?php else : ?> <?= $block->escapeHtml(__('Grand Total')) ?> <?php endif; ?> - </big></strong> + </strong> </td> - <td class="emph"><big><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></big></td> + <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index ab5cd49449ece..825ac8205772e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -9,6 +9,10 @@ */ $order = $block->getOrder(); +$baseCurrencyCode = (string)$order->getBaseCurrencyCode(); +$globalCurrencyCode = (string)$order->getGlobalCurrencyCode(); +$orderCurrencyCode = (string)$order->getOrderCurrencyCode(); + $orderAdminDate = $block->formatDate( $block->getOrderAdminDate($order->getCreatedAt()), \IntlDateFormatter::MEDIUM, @@ -23,6 +27,7 @@ $orderStoreDate = $block->formatDate( ); $customerUrl = $block->getCustomerViewUrl(); + $allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> @@ -93,15 +98,15 @@ $allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub <td><?= $block->escapeHtml($order->getRemoteIp()); ?><?= $order->getXForwardedFor() ? ' (' . $block->escapeHtml($order->getXForwardedFor()) . ')' : ''; ?></td> </tr> <?php endif; ?> - <?php if ($order->getGlobalCurrencyCode() != $order->getBaseCurrencyCode()) : ?> + <?php if ($globalCurrencyCode !== $baseCurrencyCode) : ?> <tr> - <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getGlobalCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> + <th><?= $block->escapeHtml(__('%1 / %2 rate:', $globalCurrencyCode, $baseCurrencyCode)) ?></th> <td><?= $block->escapeHtml($order->getBaseToGlobalRate()) ?></td> </tr> <?php endif; ?> - <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()) : ?> + <?php if ($baseCurrencyCode !== $orderCurrencyCode && $globalCurrencyCode !== $orderCurrencyCode) : ?> <tr> - <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> + <th><?= $block->escapeHtml(__('%1 / %2 rate:', $orderCurrencyCode, $baseCurrencyCode)) ?></th> <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 3fe9d08782880..4e07414510748 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -795,6 +795,20 @@ define([ grid.reloadParams = {'products[]':this.gridProducts.keys()}; }, + productGridFilterKeyPress: function (grid, event) { + var returnKey = parseInt(Event.KEY_RETURN || 13, 10); + + if (event.keyCode === returnKey) { + if (typeof event.stopPropagation === 'function') { + event.stopPropagation(); + } + + if (typeof event.preventDefault === 'function') { + event.preventDefault(); + } + } + }, + /** * Submit configured products to quote */ diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml index 019baeea54e23..cb84dcc3fae85 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items.phtml @@ -7,8 +7,9 @@ <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> <a href="<?= $block->escapeUrl($block->getPrintAllCreditmemosUrl($_order)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Refunds')) ?></span> </a> </div> @@ -16,8 +17,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Refund #')) ?><?= $block->escapeHtml($_creditmemo->getIncrementId()) ?> </strong> <a href="<?= $block->escapeUrl($block->getPrintCreditmemoUrl($_creditmemo)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Refund')) ?></span> </a> </div> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml index 6b87d3c22331c..2872291a0eaad 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/info/buttons.phtml @@ -16,9 +16,10 @@ <span><?= $block->escapeHtml(__('Reorder')) ?></span> </a> <?php endif ?> - <a class="action print" - href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" - onclick="this.target='_blank';"> + <a href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Order')) ?></span> </a> <?= $block->getChildHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml index 419060bfba713..ba3440f03c00f 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items.phtml @@ -7,8 +7,9 @@ <?php $_order = $block->getOrder() ?> <div class="actions-toolbar"> <a href="<?= $block->escapeUrl($block->getPrintAllInvoicesUrl($_order)) ?>" + class="action print" target="_blank" - class="action print"> + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Invoices')) ?></span> </a> </div> @@ -16,8 +17,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Invoice #')) ?><?= $block->escapeHtml($_invoice->getIncrementId()) ?></strong> <a href="<?= $block->escapeUrl($block->getPrintInvoiceUrl($_invoice)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Invoice')) ?></span> </a> </div> diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 4fd06e88878b4..8d81afeab4c90 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -56,6 +56,7 @@ public function resolve( $items[] = [ 'id' => $order->getId(), 'increment_id' => $order->getIncrementId(), + 'order_number' => $order->getIncrementId(), 'created_at' => $order->getCreatedAt(), 'grand_total' => $order->getGrandTotal(), 'status' => $order->getStatus(), diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 06146f805c644..a7c30f582e752 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -7,7 +7,8 @@ type Query { type CustomerOrder @doc(description: "Order mapping fields") { id: Int - increment_id: String + increment_id: String @deprecated(reason: "Use the order_number instaed.") + order_number: String! @doc(description: "The order number") created_at: String grand_total: Float status: String diff --git a/app/code/Magento/SalesRule/Model/CouponSearchResult.php b/app/code/Magento/SalesRule/Model/CouponSearchResult.php new file mode 100644 index 0000000000000..cba57900cf605 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/CouponSearchResult.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\SalesRule\Api\Data\CouponSearchResultInterface; + +/** + * Service Data Object with Coupon search results. + * + * @phpcs:ignoreFile + */ +class CouponSearchResult extends SearchResults implements CouponSearchResultInterface +{ + /** + * @inheritdoc + */ + public function setItems(array $items = null) + { + return parent::setItems($items); + } +} diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php new file mode 100644 index 0000000000000..a0fd4bf576f61 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Rule\Action; + +use Magento\Framework\Data\OptionSourceInterface; +use Magento\SalesRule\Model\Rule; + +/** + * Class SimpleActionOptionsProvider + */ +class SimpleActionOptionsProvider implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray() + { + return [ + ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], + ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], + ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], + ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] + ]; + } +} diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index cf6301cb31a9c..29cdf34c5a784 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -65,7 +65,6 @@ public function loadAttributeOptions() 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), - 'payment_method' => __('Payment Method'), 'shipping_method' => __('Shipping Method'), 'postcode' => __('Shipping Postcode'), 'region' => __('Shipping Region'), diff --git a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php index fdd6c2b169a7d..e4aaaec98dc79 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php +++ b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php @@ -5,11 +5,14 @@ */ namespace Magento\SalesRule\Model\Rule\Metadata; -use Magento\SalesRule\Model\Rule; -use Magento\Store\Model\System\Store; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Convert\DataObject; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use Magento\SalesRule\Model\RuleFactory; +use Magento\Store\Model\System\Store; /** * Metadata provider for sales rule edit form. @@ -37,10 +40,15 @@ class ValueProvider protected $objectConverter; /** - * @var \Magento\SalesRule\Model\RuleFactory + * @var RuleFactory */ protected $salesRuleFactory; + /** + * @var SimpleActionOptionsProvider + */ + private $simpleActionOptionsProvider; + /** * Initialize dependencies. * @@ -48,20 +56,24 @@ class ValueProvider * @param GroupRepositoryInterface $groupRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param DataObject $objectConverter - * @param \Magento\SalesRule\Model\RuleFactory $salesRuleFactory + * @param RuleFactory $salesRuleFactory + * @param SimpleActionOptionsProvider|null $simpleActionOptionsProvider */ public function __construct( Store $store, GroupRepositoryInterface $groupRepository, SearchCriteriaBuilder $searchCriteriaBuilder, DataObject $objectConverter, - \Magento\SalesRule\Model\RuleFactory $salesRuleFactory + RuleFactory $salesRuleFactory, + SimpleActionOptionsProvider $simpleActionOptionsProvider = null ) { $this->store = $store; $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->objectConverter = $objectConverter; $this->salesRuleFactory = $salesRuleFactory; + $this->simpleActionOptionsProvider = $simpleActionOptionsProvider ?: + ObjectManager::getInstance()->get(SimpleActionOptionsProvider::class); } /** @@ -71,15 +83,10 @@ public function __construct( * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function getMetadataValues(\Magento\SalesRule\Model\Rule $rule) + public function getMetadataValues(Rule $rule) { $customerGroups = $this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems(); - $applyOptions = [ - ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], - ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], - ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], - ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] - ]; + $applyOptions = $this->simpleActionOptionsProvider->toOptionArray(); $couponTypesOptions = []; $couponTypes = $this->salesRuleFactory->create()->getCouponTypes(); diff --git a/app/code/Magento/SalesRule/Model/RuleSearchResult.php b/app/code/Magento/SalesRule/Model/RuleSearchResult.php new file mode 100644 index 0000000000000..834b2b575ec2d --- /dev/null +++ b/app/code/Magento/SalesRule/Model/RuleSearchResult.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\SalesRule\Api\Data\RuleSearchResultInterface; + +/** + * Service Data Object with Sales Rule search results. + * + * @phpcs:ignoreFile + */ +class RuleSearchResult extends SearchResults implements RuleSearchResultInterface +{ + /** + * @inheritdoc + */ + public function setItems(array $items = null) + { + return parent::setItems($items); + } +} diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index f771a4f1e3892..1214e6642b440 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -6,12 +6,16 @@ namespace Magento\SalesRule\Model; use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; use Magento\Framework\App\ObjectManager; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; +use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; /** * Class RulesApplier + * * @package Magento\SalesRule\Model\Validator */ class RulesApplier @@ -39,29 +43,37 @@ class RulesApplier private $calculatorFactory; /** - * @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory + * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory + */ + protected $discountFactory; + + /** + * @param CalculatorFactory $calculatorFactory * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\SalesRule\Model\Utility $utility + * @param Utility $utility * @param ChildrenValidationLocator|null $childrenValidationLocator + * @param DataFactory $discountDataFactory */ public function __construct( \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\SalesRule\Model\Utility $utility, - ChildrenValidationLocator $childrenValidationLocator = null + ChildrenValidationLocator $childrenValidationLocator = null, + DataFactory $discountDataFactory = null ) { $this->calculatorFactory = $calculatorFactory; $this->validatorUtility = $utility; $this->_eventManager = $eventManager; $this->childrenValidationLocator = $childrenValidationLocator ?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class); + $this->discountFactory = $discountDataFactory ?: ObjectManager::getInstance()->get(DataFactory::class); } /** * Apply rules to current order item * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\ResourceModel\Rule\Collection $rules + * @param AbstractItem $item + * @param Collection $rules * @param bool $skipValidation * @param mixed $couponCode * @return array @@ -71,7 +83,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) { $address = $item->getAddress(); $appliedRuleIds = []; - /* @var $rule \Magento\SalesRule\Model\Rule */ + /* @var $rule Rule */ foreach ($rules as $rule) { if (!$this->validatorUtility->canProcessRule($rule, $address)) { continue; @@ -79,7 +91,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) if (!$skipValidation && !$rule->getActions()->validate($item)) { if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { - continue; + continue; } $childItems = $item->getChildren(); $isContinue = true; @@ -110,7 +122,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) * Add rule discount description label to address object * * @param Address $address - * @param \Magento\SalesRule\Model\Rule $rule + * @param Rule $rule * @return $this */ public function addDiscountDescription($address, $rule) @@ -123,6 +135,10 @@ public function addDiscountDescription($address, $rule) } else { if (strlen($address->getCouponCode())) { $label = $address->getCouponCode(); + + if ($rule->getDescription()) { + $label = $rule->getDescription(); + } } } @@ -136,8 +152,10 @@ public function addDiscountDescription($address, $rule) } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\Rule $rule + * Apply Rule + * + * @param AbstractItem $item + * @param Rule $rule * @param \Magento\Quote\Model\Quote\Address $address * @param mixed $couponCode * @return $this @@ -154,8 +172,10 @@ protected function applyRule($item, $rule, $address, $couponCode) } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\Rule $rule + * Get discount Data + * + * @param AbstractItem $item + * @param Rule $rule * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data */ protected function getDiscountData($item, $rule) @@ -165,9 +185,9 @@ protected function getDiscountData($item, $rule) $discountCalculator = $this->calculatorFactory->create($rule->getSimpleAction()); $qty = $discountCalculator->fixQuantity($qty, $rule); $discountData = $discountCalculator->calculate($rule, $item, $qty); - $this->eventFix($discountData, $item, $rule, $qty); $this->validatorUtility->deltaRoundingFix($discountData, $item); + $this->setDiscountBreakdown($discountData, $item, $rule); /** * We can't use row total here because row total not include tax @@ -180,8 +200,35 @@ protected function getDiscountData($item, $rule) } /** + * Set Discount Breakdown + * * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param \Magento\SalesRule\Model\Rule $rule + * @return $this + */ + private function setDiscountBreakdown($discountData, $item, $rule) + { + if ($discountData->getAmount() > 0) { + /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discount */ + $discount = $this->discountFactory->create(); + $discount->setBaseOriginalAmount($discountData->getBaseOriginalAmount()); + $discount->setAmount($discountData->getAmount()); + $discount->setBaseAmount($discountData->getBaseAmount()); + $discount->setOriginalAmount($discountData->getOriginalAmount()); + $discountBreakdown = $item->getExtensionAttributes()->getDiscounts() ?? []; + $discountBreakdown[$rule->getId()]['discount'] = $discount; + $discountBreakdown[$rule->getId()]['rule'] = $rule; + $item->getExtensionAttributes()->setDiscounts($discountBreakdown); + } + return $this; + } + + /** + * Set Discount data + * + * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData + * @param AbstractItem $item * @return $this */ protected function setDiscountData($discountData, $item) @@ -198,7 +245,7 @@ protected function setDiscountData($discountData, $item) * Set coupon code to address if $rule contains validated coupon * * @param Address $address - * @param \Magento\SalesRule\Model\Rule $rule + * @param Rule $rule * @param mixed $couponCode * @return $this */ @@ -208,7 +255,7 @@ public function maintainAddressCouponCode($address, $rule, $couponCode) Rule is a part of rules collection, which includes only rules with 'No Coupon' type or with validated coupon. As a result, if rule uses coupon code(s) ('Specific' or 'Auto' Coupon Type), it always contains validated coupon */ - if ($rule->getCouponType() != \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON) { + if ($rule->getCouponType() != Rule::COUPON_TYPE_NO_COUPON) { $address->setCouponCode($couponCode); } @@ -219,15 +266,15 @@ public function maintainAddressCouponCode($address, $rule, $couponCode) * Fire event to allow overwriting of discount amounts * * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\SalesRule\Model\Rule $rule + * @param AbstractItem $item + * @param Rule $rule * @param float $qty * @return $this */ protected function eventFix( \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData, - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\SalesRule\Model\Rule $rule, + AbstractItem $item, + Rule $rule, $qty ) { $quote = $item->getQuote(); @@ -249,11 +296,13 @@ protected function eventFix( } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * Set Applied Rule Ids + * + * @param AbstractItem $item * @param int[] $appliedRuleIds * @return $this */ - public function setAppliedRuleIds(\Magento\Quote\Model\Quote\Item\AbstractItem $item, array $appliedRuleIds) + public function setAppliedRuleIds(AbstractItem $item, array $appliedRuleIds) { $address = $item->getAddress(); $quote = $item->getQuote(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml index f9bc44a11cc47..c840162f0d162 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -105,7 +105,18 @@ <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="waitForCategoryVisible" after="openChooser"/> <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="checkCategoryName" after="waitForCategoryVisible"/> </actionGroup> - + <actionGroup name="AdminCreateMultiWebsiteCartPriceRuleActionGroup" extends="AdminCreateCartPriceRuleActionGroup"> + <annotations> + <description>EXTENDS: AdminCreateCartPriceRuleActionGroup. Removes 'clickSaveButton' for the next data changing. Assign cart price rule to 2 websites instead of 1.</description> + </annotations> + <arguments> + <argument name="ruleName"/> + </arguments> + <remove keyForRemoval="clickSaveButton"/> + <remove keyForRemoval="seeSuccessMessage"/> + <remove keyForRemoval="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" parameterArray="['FirstWebsite', 'SecondWebsite']" stepKey="selectWebsites" after="fillRuleName"/> + </actionGroup> <actionGroup name="CreateCartPriceRuleSecondWebsiteActionGroup"> <annotations> <description>Goes to the Admin Cart Price Rule grid page. Clicks on Add New Rule. Fills the provided Rule (Name). Selects 'Second Website' from the 'Websites' menu.</description> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml index 17beb4bebc7bd..af9c462bfd42c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminDeleteCartPriceRuleActionGroup.xml @@ -15,13 +15,16 @@ <arguments> <argument name="ruleName" type="entity"/> </arguments> - + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToCartPriceRules"/> <waitForPageLoad stepKey="waitForCartPriceRules"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="resetFilterBeforeDelete"/> + <waitForPageLoad stepKey="waitForCartPriceRulesResetFilter"/> <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName.name}}" stepKey="filterByName"/> <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> <click selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="clickDeleteButton"/> - <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index 0755843861247..3849d153be465 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -19,6 +19,7 @@ <element name="description" type="textarea" selector="//div[@class='admin__field-control']/textarea[@name='description']"/> <element name="active" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='is_active']/../label"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> + <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> <element name="customerGroupsOptions" type="multiselect" selector="select[name='customer_group_ids'] option"/> <element name="coupon" type="select" selector="select[name='coupon_type']"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml index 9a74ced2a2c17..89398051fcf67 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/PriceRuleConditionsSection.xml @@ -11,6 +11,7 @@ <element name="rulesDropdown" type="select" selector="select[data-form-part='sales_rule_form'][data-ui-id='newchild-0-select-rule-conditions-1-new-child']"/> <element name="addProductAttributesButton" type="text" selector="#conditions__1--1__children>li>span>a"/> <element name="productAttributesDropdown" type="select" selector="#conditions__1--1__new_child"/> + <element name="firstProductAttributeSelected" type="select" selector="#conditions__1__children .rule-param:nth-of-type(2) a:nth-child(1)"/> <element name="changeCategoriesButton" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>a"/> <element name="categoriesChooser" type="text" selector="#conditions__1--1__children>li>span.rule-param:nth-of-type(2)>span>label>a"/> <element name="treeRoot" type="text" selector=".x-tree-root-ct.x-tree-lines"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml index 92d221de9e157..02078ff15ecc2 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule of type Buy X get Y free --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 03dffe9f448ea..9d807de409a0c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml index 08a08275ee07a..1681d910ccdb0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for $10 Fixed amount discount --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index a39530f7607e4..69918bda8c426 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for Fixed amount discount for whole cart --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index 1f7d849ac02b0..898e5a07304b6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a cart price rule for 50 percent of product price --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 0d365dc089e43..e4e9a62780948 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -38,6 +38,45 @@ <argument name="total" value="447.00"/> </actionGroup> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> + </actionGroup> + <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon" /> + </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <before> + <createData entity="ApiSalesRule" stepKey="createSalesRule"/> + <createData entity="ApiSalesRuleCoupon" stepKey="createSalesRuleCoupon"> + <requiredEntity createDataKey="createSalesRule"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + </after> + + <!-- Step 5: User uses coupon codes --> + <comment userInput="Start of using coupon code" stepKey="startOfUsingCouponCode" after="endOfComparingProducts" /> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="couponOpenCart" after="startOfUsingCouponCode"/> + + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="couponApplyCoupon" after="couponOpenCart"> + <argument name="coupon" value="$$createSalesRuleCoupon$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> + <argument name="rule" value="$$createSalesRule$$"/> + <argument name="discount" value="48.00"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> + </actionGroup> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> <argument name="subtotal" value="480.00"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php new file mode 100644 index 0000000000000..f1653dd043b50 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SalesRule\Test\Unit\Model\Rule\Action; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @covers Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider + */ +class SimpleActionOptionsProviderTest extends TestCase +{ + /** + * @var SimpleActionOptionsProvider|MockObject + */ + protected $model; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->model = $objectManager->getObject(SimpleActionOptionsProvider::class); + } + + public function testToOptionArray() + { + $expected = [ + ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION], + ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION], + ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION], + ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION] + ]; + + $this->assertEquals($expected, $this->model->toOptionArray()); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index 8ca6b20db3b5a..da358372e0895 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -247,10 +247,10 @@ public function testValidateCategoriesIgnoresVisibility(): void * @param boolean $isValid * @param string $conditionValue * @param string $operator - * @param double $productPrice + * @param string $productPrice * @dataProvider localisationProvider */ - public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator = '>=', $productPrice = 2000.00) + public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator = '>=', $productPrice = '2000.00') { $attr = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php index 0864b4a5e1480..d63ba150f4822 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php @@ -5,52 +5,72 @@ */ namespace Magento\SalesRule\Test\Unit\Model\Rule\Metadata; +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\Data\GroupSearchResultsInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Convert\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider; +use Magento\SalesRule\Model\Rule\Metadata\ValueProvider; +use Magento\SalesRule\Model\RuleFactory; +use Magento\Store\Model\System\Store; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * @covers Magento\SalesRule\Model\Rule\Metadata\ValueProvider */ -class ValueProviderTest extends \PHPUnit\Framework\TestCase +class ValueProviderTest extends TestCase { /** - * @var \Magento\SalesRule\Model\Rule\Metadata\ValueProvider + * @var ValueProvider */ protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Store|MockObject */ protected $storeMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var GroupRepositoryInterface|MockObject */ protected $groupRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var SearchCriteriaBuilder|MockObject */ protected $searchCriteriaBuilderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var DataObject|MockObject */ protected $dataObjectMock; /** - * @var \Magento\SalesRule\Model\RuleFactory|\PHPUnit_Framework_MockObject_MockObject + * @var RuleFactory|MockObject */ protected $ruleFactoryMock; + /** + * @var SimpleActionOptionsProvider|MockObject + */ + private $simpleActionOptionsProviderMock; + protected function setUp() { - $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class); - $this->storeMock = $this->createMock(\Magento\Store\Model\System\Store::class); - $this->groupRepositoryMock = $this->createMock(\Magento\Customer\Api\GroupRepositoryInterface::class); - $this->dataObjectMock = $this->createMock(\Magento\Framework\Convert\DataObject::class); - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); - $groupSearchResultsMock = $this->createMock(\Magento\Customer\Api\Data\GroupSearchResultsInterface::class); - $groupsMock = $this->createMock(\Magento\Customer\Api\Data\GroupInterface::class); + $expectedData = include __DIR__ . '/_files/MetaData.php'; + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->storeMock = $this->createMock(Store::class); + $this->groupRepositoryMock = $this->createMock(GroupRepositoryInterface::class); + $this->dataObjectMock = $this->createMock(DataObject::class); + $this->simpleActionOptionsProviderMock = $this->createMock(SimpleActionOptionsProvider::class); + $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class); + $groupSearchResultsMock = $this->createMock(GroupSearchResultsInterface::class); + $groupsMock = $this->createMock(GroupInterface::class); $this->searchCriteriaBuilderMock->expects($this->once())->method('create')->willReturn($searchCriteriaMock); $this->groupRepositoryMock->expects($this->once())->method('getList')->with($searchCriteriaMock) @@ -59,15 +79,19 @@ protected function setUp() $this->storeMock->expects($this->once())->method('getWebsiteValuesForForm')->willReturn([]); $this->dataObjectMock->expects($this->once())->method('toOptionArray')->with([$groupsMock], 'id', 'code') ->willReturn([]); - $this->ruleFactoryMock = $this->createPartialMock(\Magento\SalesRule\Model\RuleFactory::class, ['create']); + $this->ruleFactoryMock = $this->createPartialMock(RuleFactory::class, ['create']); + $this->simpleActionOptionsProviderMock->method('toOptionArray')->willReturn( + $expectedData['actions']['children']['simple_action']['arguments']['data']['config']['options'] + ); $this->model = (new ObjectManager($this))->getObject( - \Magento\SalesRule\Model\Rule\Metadata\ValueProvider::class, + ValueProvider::class, [ 'store' => $this->storeMock, 'groupRepository' => $this->groupRepositoryMock, 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'objectConverter' => $this->dataObjectMock, 'salesRuleFactory' => $this->ruleFactoryMock, + 'simpleActionOptionsProvider' => $this->simpleActionOptionsProviderMock ] ); } @@ -76,8 +100,8 @@ public function testGetMetadataValues() { $expectedData = include __DIR__ . '/_files/MetaData.php'; - /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleMock */ - $ruleMock = $this->createMock(\Magento\SalesRule\Model\Rule::class); + /** @var Rule|MockObject $ruleMock */ + $ruleMock = $this->createMock(Rule::class); $this->ruleFactoryMock->expects($this->once()) ->method('create') ->willReturn($ruleMock); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 217a8dba273c4..4260e6b415091 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -6,55 +6,81 @@ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Event\Manager; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Rule\Model\Action\Collection; +use Magento\SalesRule\Model\Quote\ChildrenValidationLocator; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory; +use Magento\SalesRule\Model\Rule\Action\Discount\Data; +use Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface; +use Magento\SalesRule\Model\RulesApplier; +use Magento\SalesRule\Model\Utility; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class RulesApplierTest extends \PHPUnit\Framework\TestCase +class RulesApplierTest extends TestCase { /** - * @var \Magento\SalesRule\Model\RulesApplier + * @var RulesApplier */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CalculatorFactory|PHPUnit_Framework_MockObject_MockObject */ protected $calculatorFactory; /** - * @var \Magento\Framework\Event\Manager|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $discountFactory; + + /** + * @var Manager|PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var Utility|PHPUnit_Framework_MockObject_MockObject */ protected $validatorUtility; /** - * @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject + * @var ChildrenValidationLocator|PHPUnit_Framework_MockObject_MockObject */ protected $childrenValidationLocator; protected function setUp() { $this->calculatorFactory = $this->createMock( - \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory::class + CalculatorFactory::class + ); + $this->discountFactory = $this->createPartialMock( + \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class, + ['create'] ); $this->eventManager = $this->createPartialMock(\Magento\Framework\Event\Manager::class, ['dispatch']); $this->validatorUtility = $this->createPartialMock( - \Magento\SalesRule\Model\Utility::class, + Utility::class, ['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty'] ); $this->childrenValidationLocator = $this->createPartialMock( - \Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class, + ChildrenValidationLocator::class, ['isChildrenValidationRequired'] ); - $this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier( + $this->rulesApplier = new RulesApplier( $this->calculatorFactory, $this->eventManager, $this->validatorUtility, - $this->childrenValidationLocator + $this->childrenValidationLocator, + $this->discountFactory ); } @@ -73,21 +99,36 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $ruleId = 1; $appliedRuleIds = [$ruleId => $ruleId]; - + $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class) + ->setConstructorArgs( + [ + 'amount' => 0, + 'baseAmount' => 0, + 'originalAmount' => 0, + 'baseOriginalAmount' => 0 + ] + ) + ->getMock(); + $this->discountFactory->expects($this->any()) + ->method('create') + ->with($this->anything()) + ->will($this->returnValue($discountData)); /** - * @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing + * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing */ $ruleWithStopFurtherProcessing = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions'] ); - /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun */ + /** + * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun + */ $ruleThatShouldNotBeRun = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getStopRulesProcessing', '__wakeup'] ); - $actionMock = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); + $actionMock = $this->createPartialMock(Collection::class, ['validate']); $ruleWithStopFurtherProcessing->setName('ruleWithStopFurtherProcessing'); $ruleThatShouldNotBeRun->setName('ruleThatShouldNotBeRun'); @@ -140,6 +181,40 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, $this->assertEquals($appliedRuleIds, $result); } + public function testAddCouponDescriptionWithRuleDescriptionIsUsed() + { + $ruleId = 1; + $ruleDescription = 'Rule description'; + + /** + * @var Rule|PHPUnit_Framework_MockObject_MockObject $rule + */ + $rule = $this->createPartialMock( + Rule::class, + ['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions'] + ); + + $rule->setDescription($ruleDescription); + + /** + * @var Address|PHPUnit_Framework_MockObject_MockObject $address + */ + $address = $this->createPartialMock( + Address::class, + [ + 'getQuote', + 'setCouponCode', + 'setAppliedRuleIds', + '__wakeup' + ] + ); + $description = $address->getDiscountDescriptionArray(); + $description[$ruleId] = $rule->getDescription(); + $address->setDiscountDescriptionArray($description[$ruleId]); + + $this->assertEquals($address->getDiscountDescriptionArray(), $description[$ruleId]); + } + /** * @return array */ @@ -152,29 +227,48 @@ public function dataProviderChildren() } /** - * @return \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject + * @return AbstractItem|PHPUnit_Framework_MockObject_MockObject */ protected function getPreparedItem() { - /** @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject $address */ - $address = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ + /** + * @var Address|PHPUnit_Framework_MockObject_MockObject $address + */ + $address = $this->createPartialMock( + Address::class, + [ 'getQuote', 'setCouponCode', 'setAppliedRuleIds', '__wakeup' - ]); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ - $item = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, [ + ] + ); + /** + * @var AbstractItem|PHPUnit_Framework_MockObject_MockObject $item + */ + $item = $this->createPartialMock( + Item::class, + [ 'setDiscountAmount', 'setBaseDiscountAmount', 'setDiscountPercent', 'getAddress', 'setAppliedRuleIds', '__wakeup', - 'getChildren' - ]); + 'getChildren', + 'getExtensionAttributes' + ] + ); + $itemExtension = $this->getMockBuilder( + \Magento\Framework\Api\ExtensionAttributesInterface::class + )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); + $itemExtension->method('getDiscounts')->willReturn([]); + $itemExtension->expects($this->any()) + ->method('setDiscounts') + ->willReturn([]); $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStore', '__wakeUp']); $item->expects($this->any())->method('getAddress')->will($this->returnValue($address)); + $item->expects($this->any())->method('getExtensionAttributes')->will($this->returnValue($itemExtension)); $address->expects($this->any()) ->method('getQuote') ->will($this->returnValue($quote)); @@ -190,10 +284,10 @@ protected function applyRule($item, $rule) { $qty = 2; $discountCalc = $this->createPartialMock( - \Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface::class, + DiscountInterface::class, ['fixQuantity', 'calculate'] ); - $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class) + $discountData = $this->getMockBuilder(Data::class) ->setConstructorArgs( [ 'amount' => 30, diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml index 5a4877bbf825e..e100121bea345 100644 --- a/app/code/Magento/SalesRule/etc/db_schema.xml +++ b/app/code/Magento/SalesRule/etc/db_schema.xml @@ -58,7 +58,7 @@ </table> <table name="salesrule_coupon" resource="default" engine="innodb" comment="Salesrule Coupon"> <column xsi:type="int" name="coupon_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Coupon Id"/> + comment="Coupon ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> @@ -94,9 +94,9 @@ </table> <table name="salesrule_coupon_usage" resource="default" engine="innodb" comment="Salesrule Coupon Usage"> <column xsi:type="int" name="coupon_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Coupon Id"/> + comment="Coupon ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="int" name="times_used" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -115,11 +115,11 @@ </table> <table name="salesrule_customer" resource="default" engine="innodb" comment="Salesrule Customer"> <column xsi:type="int" name="rule_customer_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rule Customer Id"/> + comment="Rule Customer ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Rule ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Customer Id"/> + default="0" comment="Customer ID"/> <column xsi:type="smallint" name="times_used" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -141,11 +141,11 @@ </table> <table name="salesrule_label" resource="default" engine="innodb" comment="Salesrule Label"> <column xsi:type="int" name="label_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Label Id"/> + comment="Label ID"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="label" nullable="true" length="255" comment="Label"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="label_id"/> @@ -166,11 +166,11 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -200,10 +200,10 @@ </index> </table> <table name="salesrule_coupon_aggregated" resource="sales" engine="innodb" comment="Coupon Aggregated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -242,10 +242,10 @@ </table> <table name="salesrule_coupon_aggregated_updated" resource="sales" engine="innodb" comment="Salesrule Coupon Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -284,10 +284,10 @@ </table> <table name="salesrule_coupon_aggregated_order" resource="default" engine="innodb" comment="Coupon Aggregated Order"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" @@ -322,7 +322,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> @@ -341,7 +341,7 @@ <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index c1d22a04771ab..f44b172d6b479 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\SalesRule\Api\Data\ConditionInterface" type="Magento\SalesRule\Model\Data\Condition" /> <preference for="Magento\SalesRule\Api\Data\RuleSearchResultInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\SalesRule\Model\RuleSearchResult" /> <preference for="Magento\SalesRule\Api\Data\RuleLabelInterface" type="Magento\SalesRule\Model\Data\RuleLabel" /> <preference for="Magento\SalesRule\Api\Data\CouponInterface" @@ -23,7 +23,7 @@ <preference for="Magento\SalesRule\Model\Spi\CouponResourceInterface" type="Magento\SalesRule\Model\ResourceModel\Coupon" /> <preference for="Magento\SalesRule\Api\Data\CouponSearchResultInterface" - type="Magento\Framework\Api\SearchResults" /> + type="Magento\SalesRule\Model\CouponSearchResult" /> <preference for="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface" type="Magento\SalesRule\Model\Data\CouponGenerationSpec" /> <preference for="Magento\SalesRule\Api\Data\CouponMassDeleteResultInterface" diff --git a/app/code/Magento/SalesRule/etc/extension_attributes.xml b/app/code/Magento/SalesRule/etc/extension_attributes.xml new file mode 100644 index 0000000000000..202ced4204f73 --- /dev/null +++ b/app/code/Magento/SalesRule/etc/extension_attributes.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> + <extension_attributes for="Magento\Quote\Api\Data\CartItemInterface"> + <attribute code="discounts" type="string" /> + </extension_attributes> +</config> \ No newline at end of file diff --git a/app/code/Magento/SalesSequence/etc/db_schema.xml b/app/code/Magento/SalesSequence/etc/db_schema.xml index 7ad48badf7b80..5ae72319c5a69 100644 --- a/app/code/Magento/SalesSequence/etc/db_schema.xml +++ b/app/code/Magento/SalesSequence/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sales_sequence_profile" resource="sales" engine="innodb" comment="sales_sequence_profile" onCreate="skip-migration"> <column xsi:type="int" name="profile_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Id"/> + comment="ID"/> <column xsi:type="int" name="meta_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Meta_id"/> <column xsi:type="varchar" name="prefix" nullable="true" length="32" comment="Prefix"/> @@ -37,10 +37,10 @@ </table> <table name="sales_sequence_meta" resource="sales" engine="innodb" comment="sales_sequence_meta" onCreate="skip-migration"> <column xsi:type="int" name="meta_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Id"/> + comment="ID"/> <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Prefix"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="sequence_table" nullable="false" length="64" comment="table for sequence"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="meta_id"/> diff --git a/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..4a742b290c983 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SearchEngineMysqlConfigData"> + <data key="path">catalog/search/engine</data> + <data key="scope_id">1</data> + <data key="label">MySQL</data> + <data key="value">mysql</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml b/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml new file mode 100644 index 0000000000000..9ed6ccda62200 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Suite/SearchEngineMysqlSuite.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="SearchEngineMysqlSuite"> + <before> + <magentoCLI stepKey="setSearchEngineToMysql" command="config:set {{SearchEngineMysqlConfigData.path}} {{SearchEngineMysqlConfigData.value}}"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after></after> + <include> + <group name="SearchEngineMysql" /> + </include> + <exclude> + <group name="skip"/> + </exclude> + </suite> +</suites> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 61a89b4610d6a..c5124ac9c74a1 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -32,6 +32,10 @@ <!-- Create product with description --> <comment userInput="Create product with description" stepKey="createProductWithDescriptionComment"/> <createData entity="SimpleProductWithDescription" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <!-- Delete created product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index 119faef9f2f59..e49db08954e14 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -24,6 +24,10 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <!-- Delete create product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index ca48fb8565ca8..a1aa8be999aea 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -24,6 +24,10 @@ <!-- Create product with short description --> <createData entity="ApiProductWithDescription" stepKey="product"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index 6033ea8dee28b..3a8443706c9c7 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -24,6 +24,10 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php index 982d00bd9f648..748e441652f72 100644 --- a/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php @@ -50,6 +50,9 @@ public function testGetPageSize($searchEngine, $size) $this->assertEquals($size, $this->model->getMaxPageSize()); } + /** + * @return array + */ public function getPageSizeDataProvider() { return [ diff --git a/app/code/Magento/Search/etc/db_schema.xml b/app/code/Magento/Search/etc/db_schema.xml index 754af7d246d6d..ab4b54298c2a3 100644 --- a/app/code/Magento/Search/etc/db_schema.xml +++ b/app/code/Magento/Search/etc/db_schema.xml @@ -49,12 +49,12 @@ </table> <table name="search_synonyms" resource="default" engine="innodb" comment="table storing various synonyms groups"> <column xsi:type="bigint" name="group_id" padding="20" unsigned="true" nullable="false" identity="true" - comment="Synonyms Group Id"/> + comment="Synonyms Group ID"/> <column xsi:type="text" name="synonyms" nullable="false" comment="list of synonyms making up this group"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id - identifies the store view these synonyms belong to"/> + default="0" comment="Store ID - identifies the store view these synonyms belong to"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id - identifies the website id these synonyms belong to"/> + default="0" comment="Website ID - identifies the website ID these synonyms belong to"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> </constraint> diff --git a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml index 38c6fa52455b9..e7f31097368e2 100644 --- a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml +++ b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml @@ -81,16 +81,7 @@ <argument name="sortable" xsi:type="string">1</argument> <argument name="index" xsi:type="string">display_in_terms</argument> <argument name="type" xsi:type="string">options</argument> - <argument name="options" xsi:type="array"> - <item name="yes" xsi:type="array"> - <item name="value" xsi:type="string">1</item> - <item name="label" xsi:type="string" translate="true">yes</item> - </item> - <item name="no" xsi:type="array"> - <item name="value" xsi:type="string">0</item> - <item name="label" xsi:type="string" translate="true">no</item> - </item> - </argument> + <argument name="options" xsi:type="options" model="Magento\Config\Model\Config\Source\Yesno"/> </arguments> </block> <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.catalog.search.grid.columnSet.action" as="action"> diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml index ce7143582ce69..5052f5642cb53 100644 --- a/app/code/Magento/Security/etc/db_schema.xml +++ b/app/code/Magento/Security/etc/db_schema.xml @@ -10,7 +10,7 @@ <table name="admin_user_session" resource="default" engine="innodb" comment="Admin User sessions table"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="varchar" name="session_id" nullable="false" length="128" comment="Session id value"/> + <column xsi:type="varchar" name="session_id" nullable="false" length="128" comment="Session ID value"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Admin User ID"/> <column xsi:type="smallint" name="status" padding="5" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php b/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php new file mode 100644 index 0000000000000..22377897e564a --- /dev/null +++ b/app/code/Magento/SendFriend/Test/Unit/Model/CaptchaValidatorTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SendFriend\Test\Unit\Model; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\SendFriend\Model\CaptchaValidator; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Test CaptchaValidatorTest + */ +class CaptchaValidatorTest extends TestCase +{ + const FORM_ID = 'product_sendtofriend_form'; + + /** + * @var CaptchaValidator + */ + private $model; + + /** + * @var CaptchaStringResolver|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaStringResolverMock; + + /** + * @var UserContextInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $currentUserMock; + + /** + * @var CustomerRepositoryInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var Data|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaHelperMock; + + /** + * @var DefaultModel|PHPUnit_Framework_MockObject_MockObject + */ + private $captchaMock; + + /** + * @var RequestInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * Set Up + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->captchaHelperMock = $this->createMock(Data::class); + $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); + $this->currentUserMock = $this->getMockBuilder(UserContextInterface::class) + ->getMockForAbstractClass(); + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); + $this->captchaMock = $this->createMock(DefaultModel::class); + $this->requestMock = $this->getMockBuilder(RequestInterface::class)->getMock(); + + $this->model = $objectManager->getObject( + CaptchaValidator::class, + [ + 'captchaHelper' => $this->captchaHelperMock, + 'captchaStringResolver' => $this->captchaStringResolverMock, + 'currentUser' => $this->currentUserMock, + 'customerRepository' => $this->customerRepositoryMock, + ] + ); + } + + /** + * Testing the captcha validation before sending the email + * + * @dataProvider captchaProvider + * + * @param bool $captchaIsRequired + * @param bool $captchaWordIsValid + * + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function testCaptchaValidationOnSend(bool $captchaIsRequired, bool $captchaWordIsValid) + { + $word = 'test-word'; + $this->captchaHelperMock->expects($this->once())->method('getCaptcha')->with(static::FORM_ID) + ->will($this->returnValue($this->captchaMock)); + $this->captchaMock->expects($this->once())->method('isRequired') + ->will($this->returnValue($captchaIsRequired)); + + if ($captchaIsRequired) { + $this->captchaStringResolverMock->expects($this->once())->method('resolve') + ->with($this->requestMock, static::FORM_ID)->will($this->returnValue($word)); + $this->captchaMock->expects($this->once())->method('isCorrect')->with($word) + ->will($this->returnValue($captchaWordIsValid)); + } + + $this->model->validateSending($this->requestMock); + } + + /** + * Testing the wrong used word for captcha + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Incorrect CAPTCHA + */ + public function testWrongCaptcha() + { + $word = 'test-word'; + $captchaIsRequired = true; + $captchaWordIsCorrect = false; + $this->captchaHelperMock->expects($this->once())->method('getCaptcha')->with(static::FORM_ID) + ->will($this->returnValue($this->captchaMock)); + $this->captchaMock->expects($this->once())->method('isRequired') + ->will($this->returnValue($captchaIsRequired)); + $this->captchaStringResolverMock->expects($this->any())->method('resolve') + ->with($this->requestMock, static::FORM_ID)->will($this->returnValue($word)); + $this->captchaMock->expects($this->any())->method('isCorrect')->with($word) + ->will($this->returnValue($captchaWordIsCorrect)); + + $this->model->validateSending($this->requestMock); + } + + /** + * Providing captcha settings + * + * @return array + */ + public function captchaProvider(): array + { + return [ + [ + true, + true + ], [ + false, + false + ] + ]; + } +} diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php index 356483c9a5dd7..55eecfa00d6da 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php @@ -43,7 +43,7 @@ public function __construct( */ protected function _prepareLayout() { - $onclick = "submitAndReloadArea($('shipment_tracking_info').parentNode, '" . $this->getSubmitUrl() . "')"; + $onclick = "saveTrackingInfo($('shipment_tracking_info').parentNode, '" . $this->getSubmitUrl() . "')"; $this->addChild( 'save_button', \Magento\Backend\Block\Widget\Button::class, @@ -86,7 +86,10 @@ public function getRemoveUrl($track) } /** + * Get carrier title + * * @param string $code + * * @return \Magento\Framework\Phrase|string|bool */ public function getCarrierTitle($code) diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml new file mode 100644 index 0000000000000..87f139c9dc770 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAddTrackingNumberToShipmentActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddTrackingNumberToShipmentActionGroup"> + <arguments> + <argument name="trackingTitle" type="string" defaultValue=""/> + <argument name="trackingNumber" type="string"/> + </arguments> + + <fillField selector="{{AdminShipmentTrackingSection.trackingTitle}}" userInput="{{trackingTitle}}" stepKey="fillTrackingTitle"/> + <fillField selector="{{AdminShipmentTrackingSection.trackingNumber}}" userInput="{{trackingNumber}}" stepKey="fillTrackingNumber"/> + <click selector="{{AdminShipmentTrackingSection.addTrackingNumber}}" stepKey="clickAddTrackingNumber"/> + <waitForPageLoad stepKey="waitForTrackingInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.xml new file mode 100644 index 0000000000000..03301aa22f583 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertExistingTrackingNumberActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertExistingTrackingNumberActionGroup"> + <arguments> + <argument name="trackingNumber" type="string"/> + </arguments> + + <see selector="#shipment_tracking_info .col-number" userInput="{{trackingNumber}}" stepKey="seeAvailableTrackingNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml new file mode 100644 index 0000000000000..3783a9b1cc4db --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminAssertTrackingValidationErrorActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertTrackingValidationErrorActionGroup"> + <arguments> + <argument name="inputName" type="string"/> + <argument name="errorMessage" type="string" defaultValue="This is a required field."/> + </arguments> + + <see selector="{{AdminShipmentTrackingSection.trackingInfoErrorElement(inputName)}}" userInput="{{errorMessage}}" stepKey="seeTrackingInfoValidationError"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml index e506ca3a7662f..e0fec2a6dc4d2 100644 --- a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminChangeTableRatesShippingMethodStatusActionGroup.xml @@ -17,4 +17,14 @@ <uncheckOption selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="uncheckUseSystemValue"/> <selectOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" userInput="{{status}}" stepKey="changeTableRatesMethodStatus"/> </actionGroup> + <actionGroup name="AdminImportFileTableRatesShippingMethodActionGroup"> + <annotations> + <description>Import a file in Table Rates tab in Shipping Method config page.</description> + </annotations> + <arguments> + <argument name="file" type="string" defaultValue="test_tablerates.csv"/> + </arguments> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTab"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="{{file}}" stepKey="attachFileForImport"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml new file mode 100644 index 0000000000000..8c2629293acb7 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminDeleteTrackingNumberActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteTrackingNumberActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Are you sure?"/> + </arguments> + + <click selector="{{AdminShipmentTrackingSection.deleteTrackingNumber}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminGridConfirmActionSection.message}}" stepKey="waitForConfirmModal"/> + <see selector="{{AdminGridConfirmActionSection.message}}" userInput="{{message}}" stepKey="seeRemoveMessage"/> + <click selector="{{AdminGridConfirmActionSection.ok}}" stepKey="clickOkButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml new file mode 100644 index 0000000000000..fc30d752f201f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminSelectFirstGridRowActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectFirstGridRowActionGroup"> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickFirstRowInGrid"/> + <waitForPageLoad stepKey="waitToProcessPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml index e9809ae0f3e7f..631db885ab3d9 100644 --- a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml @@ -62,4 +62,24 @@ <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPageShipping"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> </actionGroup> + <actionGroup name="AdminShipmentCreateShippingLabelActionGroup"> + <arguments> + <argument name="productName" type="string" defaultValue="{{SimpleProduct.name}}"/> + </arguments> + <waitForElementVisible selector="{{AdminShipmentCreatePackageMainSection.addProductsToPackage}}" stepKey="waitForAddProductElement"/> + <click selector="{{AdminShipmentCreatePackageMainSection.addProductsToPackage}}" stepKey="clickAddProducts"/> + <waitForElementVisible selector="{{AdminShipmentCreatePackageProductGridSection.concreteProductCheckbox('productName')}}" stepKey="waitForProductBeVisible"/> + <checkOption selector="{{AdminShipmentCreatePackageProductGridSection.concreteProductCheckbox('productName')}}" stepKey="checkProductCheckbox"/> + <waitForElementVisible selector="{{AdminShipmentCreatePackageMainSection.addSelectedProductToPackage}}" stepKey="waitForAddSelectedProductElement"/> + <click selector="{{AdminShipmentCreatePackageMainSection.addSelectedProductToPackage}}" stepKey="clickAddSelectedProduct"/> + <waitForElementNotVisible selector="{{AdminShipmentCreatePackageMainSection.saveButtonDisabled}}" stepKey="waitForBeEnabled"/> + <click selector="{{AdminShipmentCreatePackageMainSection.save}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created. You created the shipping label." stepKey="seeShipmentCreateSuccess"/> + </actionGroup> + <actionGroup name="AdminGoToShipmentTabActionGroup"> + <click selector="{{AdminOrderDetailsOrderViewSection.shipments}}" stepKey="clickOrderShipmentsTab"/> + <waitForLoadingMaskToDisappear stepKey="waitForShipmentTabLoad" after="clickOrderShipmentsTab"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..7fdfe6d88b8e6 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/StorefrontSetShippingMethodActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSetShippingMethodActionGroup"> + <annotations> + <description>Selects the provided Shipping Method on checkout shipping and wait loading mask.</description> + </annotations> + <arguments> + <argument name="shippingMethodName" type="string" defaultValue="Flat Rate"/> + </arguments> + <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethodName)}}" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml new file mode 100644 index 0000000000000..ad366fd7294e5 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Data/AdminShippingSettingsConfigData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminShippingSettingsOriginCountryConfigData"> + <data key="path">shipping/origin/country_id</data> + </entity> + <entity name="AdminShippingSettingsOriginZipCodeConfigData"> + <data key="path">shipping/origin/postcode</data> + </entity> + <entity name="AdminShippingSettingsOriginCityConfigData"> + <data key="path">shipping/origin/city</data> + </entity> + <entity name="AdminShippingSettingsOriginStreetAddressConfigData"> + <data key="path">shipping/origin/street_line1</data> + </entity> + <entity name="AdminShippingSettingsOriginStreetAddress2ConfigData"> + <data key="path">shipping/origin/street_line2</data> + </entity> +</entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.xml new file mode 100644 index 0000000000000..5f33921b5a44f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentCreatePackageSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentCreatePackageMainSection"> + <element name="addProductsToPackage" type="button" selector="#package_block_1 button[data-action='package-add-items']"/> + <element name="addSelectedProductToPackage" type="button" selector="#package_block_1 button[data-action='package-save-items']"/> + <element name="save" type="button" selector="button[data-action='save-packages']"/> + <element name="saveButtonDisabled" type="button" selector="button[data-action='save-packages']._disabled"/> + </section> + <section name="AdminShipmentCreatePackageProductGridSection"> + <element name="concreteProductCheckbox" type="checkbox" selector="//td[contains(text(), '{{productName}}')]/parent::tr//input[contains(@class,'checkbox')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml index f2f39d77d8d79..d76ba0493829e 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTotalSection.xml @@ -12,5 +12,6 @@ <element name="CommentText" type="textarea" selector="#shipment_comment_text"/> <element name="AppendComments" type="checkbox" selector=".order-totals input#notify_customer"/> <element name="EmailCopy" type="checkbox" selector=".order-totals input#send_email"/> + <element name="createShippingLabel" type="checkbox" selector="input#create_shipping_label"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.xml new file mode 100644 index 0000000000000..bbb61ed013a30 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingInformationShippingSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentTrackingInformationShippingSection"> + <element name="shippingInfoTable" type="block" selector="#shipment_tracking_info"/> + <element name="shippingMethod" type="text" selector="#shipment_tracking_info .odd .col-carrier"/> + <element name="shippingMethodTitle" type="text" selector="#shipment_tracking_info .odd .col-title"/> + <element name="shippingNumber" type="text" selector="#shipment_tracking_info .odd .col-number"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.xml new file mode 100644 index 0000000000000..52a5242f2d117 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentTrackingSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentTrackingSection"> + <element name="trackingNumber" type="text" selector="#tracking-shipping-form #tracking_number"/> + <element name="trackingTitle" type="text" selector="#tracking-shipping-form #tracking_title"/> + <element name="addTrackingNumber" type="button" selector="#tracking-shipping-form button.save"/> + <element name="deleteTrackingNumber" type="button" selector="#tracking-shipping-form button.action-delete"/> + <element name="trackingInfoErrorElement" type="text" selector="#tracking-shipping-form #{{inputName}}-error" parameterized="true" /> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml index a7ed0ab498bea..99c191a5225cc 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFlatRateSection.xml @@ -11,5 +11,11 @@ <section name="AdminShippingMethodFlatRateSection"> <element name="carriersFlatRateTab" type="button" selector="#carriers_flatrate-head"/> <element name="carriersFlatRateActive" type="select" selector="#carriers_flatrate_active"/> + <element name="carriersEnableFlatRateActive" type="input" selector="#carriers_flatrate_active_inherit"/> + <element name="carriersFlatRateTitle" type="input" selector="#carriers_flatrate_title_inherit"/> + <element name="carriersFlatRateName" type="input" selector="#carriers_flatrate_name_inherit"/> + <element name="carriersFlatRateSpecificErrMsg" type="input" selector="#carriers_flatrate_specificerrmsg_inherit"/> + <element name="carriersFlatRateAllowSpecific" type="input" selector="#carriers_flatrate_sallowspecific_inherit"/> + <element name="carriersFlatRateSpecificCountry" type="input" selector="#carriers_flatrate_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.xml new file mode 100644 index 0000000000000..bf8b5b9c33672 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodFreeShippingSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodFreeShippingSection"> + <element name="carriersFreeShippingSectionHead" type="button" selector="#carriers_freeshipping-head"/> + <element name="carriersFreeShippingActive" type="input" selector="#carriers_freeshipping_active_inherit"/> + <element name="carriersFreeShippingTitle" type="input" selector="#carriers_freeshipping_title_inherit"/> + <element name="carriersFreeShippingName" type="input" selector="#carriers_freeshipping_name_inherit"/> + <element name="carriersFreeShippingSpecificErrMsg" type="input" selector="#carriers_freeshipping_specificerrmsg_inherit"/> + <element name="carriersFreeShippingAllowSpecific" type="input" selector="#carriers_freeshipping_sallowspecific_inherit"/> + <element name="carriersFreeShippingSpecificCountry" type="input" selector="#carriers_freeshipping_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml index 3c570201c9970..944fc06047aa5 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingMethodTableRatesSection.xml @@ -14,5 +14,13 @@ <element name="carriersTableRateActive" type="select" selector="#carriers_tablerate_active"/> <element name="condition" type="select" selector="#carriers_tablerate_condition_name"/> <element name="importFile" type="input" selector="#carriers_tablerate_import"/> + <element name="carriersTableRateTitle" type="input" selector="#carriers_tablerate_title_inherit"/> + <element name="carriersTableRateName" type="input" selector="#carriers_tablerate_name_inherit"/> + <element name="carriersTableRateConditionName" type="input" selector="#carriers_tablerate_condition_name_inherit"/> + <element name="carriersTableRateIncludeVirtualPrice" type="input" selector="#carriers_tablerate_include_virtual_price_inherit"/> + <element name="carriersTableRateHandlingType" type="input" selector="#carriers_tablerate_handling_type_inherit"/> + <element name="carriersTableRateSpecificErrMsg" type="input" selector="#carriers_tablerate_specificerrmsg_inherit"/> + <element name="carriersTableRateAllowSpecific" type="input" selector="#carriers_tablerate_sallowspecific_inherit"/> + <element name="carriersTableRateSpecificCountry" type="input" selector="#carriers_tablerate_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..0b7ddd0cfa781 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <annotations> + <features value="Configuration"/> + <stories value="Disable configuration inputs"/> + <title value="Check that all input fields disabled after executing CLI app:config:dump"/> + <description value="Check that all input fields disabled after executing CLI app:config:dump"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11158"/> + <useCaseId value="MAGETWO-96428"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Assert configuration are disabled in Flat Rate section--> + <comment userInput="Assert configuration are disabled in Flat Rate section" stepKey="commentSeeDisabledFlatRateConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTab}}" dependentSelector="{{AdminShippingMethodFlatRateSection.carriersFlatRateActive}}" visible="false" stepKey="expandFlatRateTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFlatRateSection.carriersEnableFlatRateActive}}" stepKey="waitForFlatRateTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersEnableFlatRateActive}}" userInput="disabled" stepKey="grabFlatRateActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateActiveDisabled" stepKey="assertFlatRateActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTitle}}" userInput="disabled" stepKey="grabFlatRateTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateTitleDisabled" stepKey="assertFlatRateTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateName}}" userInput="disabled" stepKey="grabFlatRateNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateNameDisabled" stepKey="assertFlatRateNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateSpecificErrMsg}}" userInput="disabled" stepKey="grabFlatRateSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateSpecificErrMsgDisabled" stepKey="assertFlatRateSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateAllowSpecific}}" userInput="disabled" stepKey="grabFlatRateAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateAllowSpecificDisabled" stepKey="assertFlatRateAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateSpecificCountry}}" userInput="disabled" stepKey="grabFlatRateSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFlatRateSpecificCountryDisabled" stepKey="assertFlatRateSpecificCountryDisabled"/> + <!--Assert configuration are disabled in Free Shipping section--> + <comment userInput="Assert configuration are disabled in Free Shipping section" stepKey="commentSeeDisabledFreeShippingConfigs"/> + <conditionalClick selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSectionHead}}" dependentSelector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" visible="false" stepKey="expandFreeShippingTab"/> + <waitForElementVisible selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" stepKey="waitForFreeShippingTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingActive}}" userInput="disabled" stepKey="grabFreeShippingActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingActiveDisabled" stepKey="assertFreeShippingActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingTitle}}" userInput="disabled" stepKey="grabFreeShippingTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingTitleDisabled" stepKey="assertFreeShippingTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingName}}" userInput="disabled" stepKey="grabFreeShippingNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingNameDisabled" stepKey="assertFreeShippingNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSpecificErrMsg}}" userInput="disabled" stepKey="grabFreeShippingSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingSpecificErrMsgDisabled" stepKey="assertFreeShippingSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingAllowSpecific}}" userInput="disabled" stepKey="grabFreeShippingAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingAllowSpecificDisabled" stepKey="assertFreeShippingAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodFreeShippingSection.carriersFreeShippingSpecificCountry}}" userInput="disabled" stepKey="grabFreeShippingSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabFreeShippingSpecificCountryDisabled" stepKey="assertFreeShippingSpecificCountryDisabled"/> + <!--Assert configuration are disabled in Table Rates section--> + <comment userInput="Assert configuration are disabled in Table Rates section" stepKey="commentSeeDisabledTableRatesConfigs"/> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTableRateTab"/> + <waitForElementVisible selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="waitForTableRateTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" userInput="disabled" stepKey="grabTableRateActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateActiveDisabled" stepKey="assertTableRateActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTitle}}" userInput="disabled" stepKey="grabTableRateTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateTitleDisabled" stepKey="assertTableRateTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateName}}" userInput="disabled" stepKey="grabTableRateNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateNameDisabled" stepKey="assertTableRateNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" userInput="disabled" stepKey="grabTableRateConditionNameDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateConditionNameDisabled" stepKey="assertTableRateConditionNameDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateIncludeVirtualPrice}}" userInput="disabled" stepKey="grabTableRateIncludeVirtualPriceDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateIncludeVirtualPriceDisabled" stepKey="assertTableRateIncludeVirtualPriceDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateHandlingType}}" userInput="disabled" stepKey="grabTableRateHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateHandlingTypeDisabled" stepKey="assertTableRateHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateSpecificErrMsg}}" userInput="disabled" stepKey="grabTableRateSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateSpecificErrMsgDisabled" stepKey="assertTableRateSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateAllowSpecific}}" userInput="disabled" stepKey="grabTableRateAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateAllowSpecificDisabled" stepKey="assertTableRateAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodTableRatesSection.carriersTableRateSpecificCountry}}" userInput="disabled" stepKey="grabTableRateSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabTableRateSpecificCountryDisabled" stepKey="assertTableRateSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml new file mode 100644 index 0000000000000..87058245c6014 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckTheConfirmationPopupTest"> + <annotations> + <stories value="Admin confirmation modal should be in Magento style"/> + <title value="Admin confirmation modal should be in Magento style"/> + <description value="Testing the confirmation modal for removing the tracking number"/> + <severity value="CRITICAL"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="CreateOrderActionGroup" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="createShipmentForOrder"/> + <actionGroup ref="FilterShipmentGridByOrderIdActionGroup" stepKey="filterForNewlyCreatedShipment"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="selectShipmentFromGrid"/> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + <actionGroup ref="AdminDeleteTrackingNumberActionGroup" stepKey="deleteTrackingNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml new file mode 100644 index 0000000000000..b2e3e2516a5c3 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderCustomStoreShippingMethodTableRatesTest"> + <annotations> + <features value="Shipping"/> + <stories value="Shipping method Table Rates"/> + <title value="Create order on second store with shipping method Table Rates"/> + <description value="Create order on second store with shipping method Table Rates"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6411"/> + <useCaseId value="MAGETWO-91702"/> + <group value="shipping"/> + </annotations> + <before> + <!--Create product and customer--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website, store group and store view--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + <!--Create customer associated to website--> + <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <grabFromCurrentUrl regex="~/website_id/(\d+)/~" stepKey="grabWebsiteIdFromURL"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"> + <field key="website_id">$grabWebsiteIdFromURL</field> + </createData> + <!--Enable Table Rate method and import csv file--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="switchDefaultWebsite"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethodForDefaultWebsite"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigForDefaultWebsite"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="switchCustomWebsite"> + <argument name="website" value="customWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"> + <argument name="status" value="1"/> + </actionGroup> + <actionGroup ref="AdminImportFileTableRatesShippingMethodActionGroup" stepKey="importCSVFile"> + <argument name="file" value="usa_tablerates.csv"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Assign product to custom website--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="unassignWebsiteFromProductActionGroup" stepKey="unassignWebsiteInProduct"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectWebsiteInProduct"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Create order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToTheOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerInfo"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!--Choose Best Way shipping Method--> + <actionGroup ref="AdminOrderSelectShippingMethodActionGroup" stepKey="chooseBestWayMethod"> + <argument name="methodTitle" value="bestway"/> + <argument name="methodName" value="tablerate"/> + </actionGroup> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml new file mode 100644 index 0000000000000..ca4d731eb82b1 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminValidateShippingTrackingNumberTest"> + <annotations> + <stories value="Admin validate the shipping tracking number for an order"/> + <title value="Admin validate the shipping tracking number for an order"/> + <description value="Testing for a required tracking number when adding new shipping information"/> + <severity value="CRITICAL"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="CreateOrderActionGroup" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="createShipmentForOrder"/> + <actionGroup ref="FilterShipmentGridByOrderIdActionGroup" stepKey="filterForNewlyCreatedShipment"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <actionGroup ref="AdminSelectFirstGridRowActionGroup" stepKey="selectShipmentFromGrid"/> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingInformation"> + <argument name="trackingNumber" value=""/> + </actionGroup> + <actionGroup ref="AdminAssertTrackingValidationErrorActionGroup" stepKey="assertValidateTrackingNumber"> + <argument name="inputName" value="tracking_number"/> + </actionGroup> + <actionGroup ref="AdminAddTrackingNumberToShipmentActionGroup" stepKey="addTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + <actionGroup ref="AdminAssertExistingTrackingNumberActionGroup" stepKey="checkAddedTrackingNumber"> + <argument name="trackingNumber" value="123123"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml new file mode 100644 index 0000000000000..bb29a4a28bcf6 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontDisplayTableRatesShippingMethodForAETest"> + <annotations> + <features value="Shipping"/> + <stories value="Table Rates"/> + <title value="Displaying of Table Rates for Armed Forces Europe (AE)"/> + <description value="Displaying of Table Rates for Armed Forces Europe (AE)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6405"/> + <group value="shipping"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_ArmedForcesEurope" stepKey="createCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Rollback config--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodSystemConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewToMainWebsite"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="disableTableRatesShippingMethod"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveSystemConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Admin Configuration: enable Table Rates and import CSV file with the rates--> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="tablerates.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <!--Login as created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add the created product to the shopping cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!--Proceed to Checkout from the mini cart--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <!--Shipping Method: select table rate--> + <actionGroup ref="AssertStoreFrontShippingMethodAvailableActionGroup" stepKey="assertShippingMethodAvailable"> + <argument name="shippingMethodName" value="Best Way"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodTableRate"> + <argument name="shippingMethodName" value="Best Way"/> + </actionGroup> + <!--Proceed to Review and Payments section--> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <waitForPageLoad stepKey="waitForReviewAndPaymentsPageIsLoaded"/> + <!--Place order and assert the message of success--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderProductSuccessful"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index cd25cb919adb5..28322d9534926 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -31,7 +31,7 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : packaging.sendCreateLabelRequest(); }); packaging.setLabelCreatedCallback(function(response){ - setLocation("<?php $block->escapeJs($block->escapeUrl($block->getUrl( + setLocation("<?= $block->escapeJs($block->escapeUrl($block->getUrl( 'sales/order/view', ['order_id' => $block->getShipment()->getOrderId()] ))); ?>"); diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml index 67587f19774c4..a013abfd65f87 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml @@ -9,84 +9,102 @@ ?> <?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View */ ?> <div class="admin__control-table-wrapper"> - <table class="data-table admin__control-table" id="shipment_tracking_info"> - <thead> - <tr class="headings"> - <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> - <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> - <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> - <th class="col-delete last"><?= $block->escapeHtml(__('Action')) ?></th> - </tr> - </thead> - <tfoot> - <tr> - <td class="col-carrier"> - <select name="carrier" - class="select admin__control-select" - onchange="selectCarrier(this)"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> - <?php endforeach; ?> - </select> - </td> - <td class="col-title"> - <input class="input-text admin__control-text" - type="text" - id="tracking_title" - name="title" - value="" /> - </td> - <td class="col-number"> - <input class="input-text admin__control-text" - type="text" - id="tracking_number" - name="number" - value="" /> - </td> - <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> - </tr> - </tfoot> - <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> - <tbody> - <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> - <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> - <td class="col-carrier"> - <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> - </td> - <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> - <td class="col-number"> - <?php if ($_track->isCustom()) : ?> - <?= $block->escapeHtml($_track->getNumber()) ?> - <?php else : ?> - <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> - <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> - <?php endif; ?> - </td> - <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> - </tr> - <?php endforeach; ?> - </tbody> - <?php endif; ?> - </table> + <form id="tracking-shipping-form" data-mage-init='{"validation": {}}'> + <table class="data-table admin__control-table" id="shipment_tracking_info"> + <thead> + <tr class="headings"> + <th class="col-carrier"><?= $block->escapeHtml(__('Carrier')) ?></th> + <th class="col-title"><?= $block->escapeHtml(__('Title')) ?></th> + <th class="col-number"><?= $block->escapeHtml(__('Number')) ?></th> + <th class="col-delete last"><?= $block->escapeHtml(__('Action')) ?></th> + </tr> + </thead> + <tfoot> + <tr> + <td class="col-carrier"> + <select name="carrier" + class="select admin__control-select" + onchange="selectCarrier(this)"> + <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> + <?php endforeach; ?> + </select> + </td> + <td class="col-title"> + <input class="input-text admin__control-text" + type="text" + id="tracking_title" + name="title" + value="" /> + </td> + <td class="col-number"> + <input class="input-text admin__control-text required-entry" + type="text" + id="tracking_number" + name="number" + value="" /> + </td> + <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> + </tr> + </tfoot> + <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> + <tbody> + <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> + <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> + <td class="col-carrier"> + <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> + </td> + <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> + <td class="col-number"> + <?php if ($_track->isCustom()) : ?> + <?= $block->escapeHtml($_track->getNumber()) ?> + <?php else : ?> + <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> + <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> + <?php endif; ?> + </td> + <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> + </tr> + <?php endforeach; ?> + </tbody> + <?php endif; ?> + </table> + </form> </div> <script> -require(['prototype'], function(){ - +require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, $j, confirm) { //<![CDATA[ function selectCarrier(elem) { var option = elem.options[elem.selectedIndex]; $('tracking_title').value = option.value && option.value != 'custom' ? option.text : ''; } -function deleteTrackingNumber(url) { - if (confirm('<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>')) { - submitAndReloadArea($('shipment_tracking_info').parentNode, url) +function saveTrackingInfo(node, url) { + var form = $j('#tracking-shipping-form'); + + if (form.validation() && form.validation('isValid')) { + submitAndReloadArea(node, url); } } +function deleteTrackingNumber(url) { + confirm({ + content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>', + actions: { + /** + * Confirm action. + */ + confirm: function () { + submitAndReloadArea($('shipment_tracking_info').parentNode, url); + } + } + }); +} + window.selectCarrier = selectCarrier; window.deleteTrackingNumber = deleteTrackingNumber; +window.saveTrackingInfo = saveTrackingInfo; //]]> }); diff --git a/app/code/Magento/Shipping/view/frontend/templates/items.phtml b/app/code/Magento/Shipping/view/frontend/templates/items.phtml index f0f1423ed47a2..177628c6b2015 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/items.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/items.phtml @@ -15,8 +15,9 @@ <?= $block->getChildHtml('track-all-link') ?> <?php endif; ?> <a href="<?= $block->escapeUrl($block->getPrintAllShipmentsUrl($_order)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print All Shipments')) ?></span> </a> </div> @@ -24,8 +25,9 @@ <div class="order-title"> <strong><?= $block->escapeHtml(__('Shipment #')) ?><?= $block->escapeHtml($_shipment->getIncrementId()) ?></strong> <a href="<?= $block->escapeUrl($block->getPrintShipmentUrl($_shipment)) ?>" - onclick="this.target='_blank'" - class="action print"> + class="action print" + target="_blank" + rel="noopener"> <span><?= $block->escapeHtml(__('Print Shipment')) ?></span> </a> <a href="#" diff --git a/app/code/Magento/Signifyd/Model/CaseSearchResults.php b/app/code/Magento/Signifyd/Model/CaseSearchResults.php new file mode 100644 index 0000000000000..ff1ab8839f6cd --- /dev/null +++ b/app/code/Magento/Signifyd/Model/CaseSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Signifyd\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Signifyd\Api\Data\CaseSearchResultsInterface; + +/** + * Service Data Object with Case entities search results. + */ +class CaseSearchResults extends SearchResults implements CaseSearchResultsInterface +{ +} diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml index c586019ca3d12..e82e8f84b3584 100644 --- a/app/code/Magento/Signifyd/etc/di.xml +++ b/app/code/Magento/Signifyd/etc/di.xml @@ -9,7 +9,7 @@ <preference for="Magento\Signifyd\Api\Data\CaseInterface" type="Magento\Signifyd\Model\CaseEntity" /> <preference for="Magento\Signifyd\Api\CaseRepositoryInterface" type="Magento\Signifyd\Model\CaseRepository" /> <preference for="Magento\Signifyd\Api\CaseManagementInterface" type="Magento\Signifyd\Model\CaseManagement" /> - <preference for="Magento\Signifyd\Api\Data\CaseSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Signifyd\Api\Data\CaseSearchResultsInterface" type="Magento\Signifyd\Model\CaseSearchResults" /> <preference for="Magento\Signifyd\Api\CaseCreationServiceInterface" type="Magento\Signifyd\Model\CaseServices\CreationService" /> <preference for="Magento\Signifyd\Api\GuaranteeCreationServiceInterface" type="Magento\Signifyd\Model\Guarantee\CreationService" /> <preference for="Magento\Signifyd\Api\GuaranteeCancelingServiceInterface" type="Magento\Signifyd\Model\Guarantee\CancelingService" /> diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php index 8eeeb5bf6bc12..5cfc7349888f3 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php @@ -49,7 +49,13 @@ public function execute() // if sitemap record exists if ($sitemap->getId()) { try { + $this->appEmulation->startEnvironmentEmulation( + $sitemap->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $sitemap->generateXml(); + $this->appEmulation->stopEnvironmentEmulation(); $this->messageManager->addSuccessMessage( __('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename()) ); diff --git a/app/code/Magento/Sitemap/etc/db_schema.xml b/app/code/Magento/Sitemap/etc/db_schema.xml index b3c028b626b73..adf1f11124f52 100644 --- a/app/code/Magento/Sitemap/etc/db_schema.xml +++ b/app/code/Magento/Sitemap/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="sitemap" resource="default" engine="innodb" comment="XML Sitemap"> <column xsi:type="int" name="sitemap_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Sitemap Id"/> + comment="Sitemap ID"/> <column xsi:type="varchar" name="sitemap_type" nullable="true" length="32" comment="Sitemap Type"/> <column xsi:type="varchar" name="sitemap_filename" nullable="true" length="32" comment="Sitemap Filename"/> <column xsi:type="varchar" name="sitemap_path" nullable="true" length="255" comment="Sitemap Path"/> <column xsi:type="timestamp" name="sitemap_time" on_update="false" nullable="true" comment="Sitemap Time"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store id"/> + default="0" comment="Store ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="sitemap_id"/> </constraint> diff --git a/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php index 77ccce5d23bde..f732871114061 100644 --- a/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Store/App/Config/Source/RuntimeConfigSource.php @@ -44,6 +44,7 @@ public function __construct( /** * Return whole scopes config data from db. + * * Ignore $path argument due to config source must return all config data * * @param string $path @@ -64,6 +65,8 @@ public function get($path = '') } /** + * Retrieve default connection + * * @return AdapterInterface */ private function getConnection() @@ -83,12 +86,17 @@ private function getConnection() */ private function getEntities($table, $keyField) { - $entities = $this->getConnection()->fetchAll( - $this->getConnection()->select()->from($this->resourceConnection->getTableName($table)) - ); $data = []; - foreach ($entities as $entity) { - $data[$entity[$keyField]] = $entity; + $tableName = $this->resourceConnection->getTableName($table); + // Check if db table exists before fetch data + if ($this->resourceConnection->getConnection()->isTableExists($tableName)) { + $entities = $this->getConnection()->fetchAll( + $this->getConnection()->select()->from($tableName) + ); + + foreach ($entities as $entity) { + $data[$entity[$keyField]] = $entity; + } } return $data; diff --git a/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php b/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php index 5df50581792ce..de2da54423822 100644 --- a/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php +++ b/app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php @@ -5,6 +5,9 @@ */ namespace Magento\Store\App\FrontController\Plugin; +/** + * Class RequestPreprocessor + */ class RequestPreprocessor { /** @@ -52,6 +55,7 @@ public function __construct( /** * Auto-redirect to base url (without SID) if the requested url doesn't match it. + * * By default this feature is enabled in configuration. * * @param \Magento\Framework\App\FrontController $subject @@ -72,10 +76,11 @@ public function aroundDispatch( $this->_storeManager->getStore()->isCurrentlySecure() ); if ($baseUrl) { + // phpcs:disable Magento2.Functions.DiscouragedFunction $uri = parse_url($baseUrl); if (!$this->getBaseUrlChecker()->execute($uri, $request)) { $redirectUrl = $this->_url->getRedirectUrl( - $this->_url->getUrl(ltrim($request->getPathInfo(), '/'), ['_nosid' => true]) + $this->_url->getDirectUrl(ltrim($request->getPathInfo(), '/'), ['_nosid' => true]) ); $redirectCode = (int)$this->_scopeConfig->getValue( 'web/url/redirect_to_base', diff --git a/app/code/Magento/Store/Model/ResourceModel/Store.php b/app/code/Magento/Store/Model/ResourceModel/Store.php index 88d7b5d8216fe..7a2821987f9bf 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Store.php +++ b/app/code/Magento/Store/Model/ResourceModel/Store.php @@ -166,11 +166,16 @@ protected function _changeGroup(\Magento\Framework\Model\AbstractModel $model) */ public function readAllStores() { - $select = $this->getConnection() - ->select() - ->from($this->getTable('store')); + $stores = []; + if ($this->getConnection()->isTableExists($this->getMainTable())) { + $select = $this->getConnection() + ->select() + ->from($this->getTable($this->getMainTable())); - return $this->getConnection()->fetchAll($select); + $stores = $this->getConnection()->fetchAll($select); + } + + return $stores; } /** diff --git a/app/code/Magento/Store/Model/ResourceModel/Website.php b/app/code/Magento/Store/Model/ResourceModel/Website.php index d6fefd60ae54a..431a9d62e7c39 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website.php @@ -47,12 +47,15 @@ protected function _initUniqueFields() public function readAllWebsites() { $websites = []; - $select = $this->getConnection() - ->select() - ->from($this->getTable('store_website')); + $tableName = $this->getMainTable(); + if ($this->getConnection()->isTableExists($tableName)) { + $select = $this->getConnection() + ->select() + ->from($tableName); - foreach ($this->getConnection()->fetchAll($select) as $websiteData) { - $websites[$websiteData['code']] = $websiteData; + foreach ($this->getConnection()->fetchAll($select) as $websiteData) { + $websites[$websiteData['code']] = $websiteData; + } } return $websites; @@ -115,6 +118,7 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $model) /** * Retrieve default stores select object + * * Select fields website_id, store_id * * @param bool $includeDefault include/exclude default admin website diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 0bc371da0aab9..faa26b24a5505 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -53,7 +53,7 @@ class Store extends AbstractExtensibleModel implements const ENTITY = 'store'; /** - * Custom entry point param + * Parameter used to determine app context. */ const CUSTOM_ENTRY_POINT_PARAM = 'custom_entry_point'; @@ -104,7 +104,7 @@ class Store extends AbstractExtensibleModel implements const ADMIN_CODE = 'admin'; /** - * Cache tag + * Tag to use to cache stores. */ const CACHE_TAG = 'store'; @@ -423,6 +423,9 @@ public function __construct( /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -435,6 +438,9 @@ public function __sleep() * Init not serializable fields * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml new file mode 100644 index 0000000000000..1a43ae1d2bbd1 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminWebsitePageActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGoCreatedWebsitePageActionGroup"> + <annotations> + <description>Filter website name in grid and go first found website page</description> + </annotations> + <arguments> + <argument name="websiteName" type="string" defaultValue="SecondWebsite"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 1a1847bf38308..982d829b57153 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -11,6 +11,9 @@ <data key="code">default</data> <data key="is_active">1</data> </entity> + <entity name="DefaultAllStoreView" type="store"> + <data key="name">All Store Views</data> + </entity> <entity name="customStore" type="store"> <!--data key="group_id">customStoreGroup.id</data--> <data key="name" unique="suffix">store</data> @@ -194,4 +197,4 @@ <data key="name">third_store_view</data> <data key="code">third_store_view</data> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml index ed879a82d3f59..e93fd62a74999 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml @@ -21,15 +21,7 @@ <createData stepKey="b2" entity="customStoreGroup"/> </before> <after> - <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup"> - <argument name="storeGroupName" value="customStoreGroup.name" /> - </actionGroup> - <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStoreGroup2"> - <argument name="storeGroupName" value="customStoreGroup.name" /> - </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php b/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php index ba06d191c9f71..a8f76d0a28fee 100644 --- a/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php @@ -53,11 +53,11 @@ public function setUp() public function testGet() { - $this->deploymentConfig->expects($this->once()) + $this->deploymentConfig->expects($this->any()) ->method('get') ->with('db') ->willReturn(true); - $this->resourceConnection->expects($this->once())->method('getConnection')->willReturn($this->connection); + $this->resourceConnection->expects($this->any())->method('getConnection')->willReturn($this->connection); $selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $selectMock->expects($this->any())->method('from')->willReturnSelf(); diff --git a/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php new file mode 100644 index 0000000000000..926764b989686 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/StoreTest.php @@ -0,0 +1,190 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Store; +use Magento\Framework\DB\Adapter\AdapterInterface; + +class StoreTest extends \PHPUnit\Framework\TestCase +{ + /** @var Store */ + protected $model; + + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $resourceMock; + + /** @var Select | \PHPUnit_Framework_MockObject_MockObject */ + protected $select; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connectionMock; + + public function setUp() + { + $objectManagerHelper = new ObjectManager($this); + $this->select = $this->createMock(Select::class); + $this->resourceMock = $this->createPartialMock( + ResourceConnection::class, + [ + 'getConnection', + 'getTableName' + ] + ); + $this->connectionMock = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ + 'isTableExists', + 'select', + 'fetchAll', + 'fetchOne', + 'from', + 'getCheckSql', + 'where', + 'quoteIdentifier', + 'quote' + ] + ); + + $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); + $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock); + $configCacheTypeMock = $this->createMock('\Magento\Framework\App\Cache\Type\Config'); + $this->model = $objectManagerHelper->getObject( + Store::class, + [ + 'context' => $contextMock, + 'configCacheType' => $configCacheTypeMock + ] + ); + } + + public function testCountAll($countAdmin = false) + { + $mainTable = 'store'; + $tableIdentifier = 'code'; + $tableIdentifierValue = 'admin'; + $count = 1; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable, 'COUNT(*)') + ->willReturnSelf(); + + $this->connectionMock->expects($this->any()) + ->method('quoteIdentifier') + ->with($tableIdentifier) + ->willReturn($tableIdentifier); + + $this->connectionMock->expects($this->once()) + ->method('quote') + ->with($tableIdentifierValue) + ->willReturn($tableIdentifierValue); + + $this->select->expects($this->any()) + ->method('where') + ->with(sprintf('%s <> %s', $tableIdentifier, $tableIdentifierValue)) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn($count); + + $this->assertEquals($count, $this->model->countAll($countAdmin)); + } + + public function testReadAllStores() + { + $mainTable = 'store'; + $data = [ + ["store_id" => "0", "code" => "admin", "website_id" => 0, "name" => "Admin"], + ["store_id" => "1", "code" => "default", "website_id" => 1, "name" => "Default Store View"] + ]; + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(true); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllStores()); + } + + public function testReadAllStoresNoDbTable() + { + $mainTable = 'no_store_table'; + $data = []; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(false); + + $this->connectionMock->expects($this->never()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->never()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->never()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllStores()); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php new file mode 100644 index 0000000000000..5fd5aa09a46be --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/ResourceModel/WebsiteTest.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Website; + +class WebsiteTest extends \PHPUnit\Framework\TestCase +{ + /** @var Website */ + protected $model; + + /** + * @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $resourceMock; + + /** @var Select | \PHPUnit_Framework_MockObject_MockObject */ + protected $select; + + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connectionMock; + + public function setUp() + { + $objectManagerHelper = new ObjectManager($this); + $this->select = $this->createMock(\Magento\Framework\DB\Select::class); + $this->resourceMock = $this->createPartialMock( + ResourceConnection::class, + [ + 'getConnection', + 'getTableName' + ] + ); + $this->connectionMock = $this->createPartialMock( + \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + [ + 'isTableExists', + 'select', + 'fetchAll', + 'fetchOne', + 'from', + 'getCheckSql', + 'joinLeft', + 'where' + ] + ); + $contextMock = $this->createMock(\Magento\Framework\Model\ResourceModel\Db\Context::class); + $contextMock->expects($this->once())->method('getResources')->willReturn($this->resourceMock); + $this->model = $objectManagerHelper->getObject( + Website::class, + [ + 'context' => $contextMock + ] + ); + } + + public function testReadAllWebsites() + { + $data = [ + "admin" => ["website_id" => "0", "code" => "admin", "name" => "Admin"], + "base" => ["website_id" => "1", "code" => "base", "name" => "Main Website"] + ]; + $mainTable = 'store_website'; + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(true); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllWebsites()); + } + + public function testReadAllWebsitesNoDbTable() + { + $data = []; + $mainTable = 'no_store_website_table'; + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('isTableExists') + ->with($mainTable) + ->willReturn(false); + + $this->connectionMock->expects($this->never()) + ->method('select') + ->willReturn($this->select); + + $this->select->expects($this->never()) + ->method('from') + ->with($mainTable) + ->willReturnSelf(); + + $this->connectionMock->expects($this->never()) + ->method('fetchAll') + ->with($this->select) + ->willReturn($data); + + $this->assertEquals($data, $this->model->readAllWebsites()); + } + + public function testGetDefaultStoresSelect($includeDefault = false) + { + $storeId = 1; + $storeWebsiteTable = 'store_website'; + $storeGroupTable = 'store_group'; + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('getCheckSql') + ->with( + 'store_group_table.default_store_id IS NULL', + '0', + 'store_group_table.default_store_id' + ) + ->willReturn($storeId); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->atLeastOnce()) + ->method('getTableName') + ->withConsecutive([$storeWebsiteTable], [$storeGroupTable]) + ->willReturnOnConsecutiveCalls($storeWebsiteTable, $storeGroupTable); + + $this->select->expects($this->once()) + ->method('from') + ->with( + ['website_table' => $storeWebsiteTable], + ['website_id'] + ) + ->willReturnSelf(); + + $this->select->expects($this->once()) + ->method('joinLeft') + ->with( + ['store_group_table' => $storeGroupTable], + 'website_table.website_id=store_group_table.website_id' . + ' AND website_table.default_group_id = store_group_table.group_id', + ['store_id' => $storeId] + ) + ->willReturnSelf(); + + $this->assertInstanceOf('\Magento\Framework\DB\Select', $this->model->getDefaultStoresSelect($includeDefault)); + } + + public function testCountAll($includeDefault = false) + { + $count = 2; + $mainTable = 'store_website'; + + $this->resourceMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->select); + + $this->resourceMock->expects($this->once()) + ->method('getTableName') + ->willReturn($mainTable); + + $this->select->expects($this->once()) + ->method('from') + ->with($mainTable, 'COUNT(*)') + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($this->select) + ->willReturn($count); + + $this->assertEquals($count, $this->model->countAll($includeDefault)); + } +} diff --git a/app/code/Magento/Store/etc/db_schema.xml b/app/code/Magento/Store/etc/db_schema.xml index 6eea94b8deec7..5b2e178ff24b8 100644 --- a/app/code/Magento/Store/etc/db_schema.xml +++ b/app/code/Magento/Store/etc/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="store_website" resource="default" engine="innodb" comment="Websites"> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> <column xsi:type="varchar" name="name" nullable="true" length="64" comment="Website Name"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="smallint" name="default_group_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Group Id"/> + identity="false" default="0" comment="Default Group ID"/> <column xsi:type="smallint" name="is_default" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Defines Is Website Default"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -33,14 +33,14 @@ </table> <table name="store_group" resource="default" engine="innodb" comment="Store Groups"> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Group Id"/> + comment="Group ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="varchar" name="name" nullable="false" length="255" comment="Store Group Name"/> <column xsi:type="int" name="root_category_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Root Category Id"/> + default="0" comment="Root Category ID"/> <column xsi:type="smallint" name="default_store_id" padding="5" unsigned="true" nullable="false" - identity="false" default="0" comment="Default Store Id"/> + identity="false" default="0" comment="Default Store ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Store group unique code"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> @@ -59,12 +59,12 @@ </table> <table name="store" resource="default" engine="innodb" comment="Stores"> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Group Id"/> + default="0" comment="Group ID"/> <column xsi:type="varchar" name="name" nullable="false" length="255" comment="Store Name"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Sort Order"/> diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index 3a0143821d8b9..f3771b704c3e9 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -23,4 +23,11 @@ </argument> </arguments> </type> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="store_name" xsi:type="string">store/information/name</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 376635e5c8f75..aaef3aa13dbaf 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -30,4 +30,5 @@ type StoreConfig @doc(description: "The type contains information about a store secure_base_link_url : String @doc(description: "Secure base link URL for the store") secure_base_static_url : String @doc(description: "Secure base static URL for the store") secure_base_media_url : String @doc(description: "Secure base media URL for the store") + store_name : String @doc(description: "Name of the store") } diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php index 0848f566f67bb..a2cae7f7b5a20 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types = 1); namespace Magento\Swatches\Block\Product\Renderer; use Magento\Catalog\Block\Product\Context; @@ -57,6 +58,11 @@ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\ */ const SWATCH_THUMBNAIL_NAME = 'swatchThumb'; + /** + * Config path which contains number of swatches per product + */ + private const XML_PATH_SWATCHES_PER_PRODUCT = 'catalog/frontend/swatches_per_product'; + /** * @var Product */ @@ -200,7 +206,7 @@ public function getJsonSwatchConfig() public function getNumberSwatchesPerProduct() { return $this->_scopeConfig->getValue( - 'catalog/frontend/swatches_per_product', + self::XML_PATH_SWATCHES_PER_PRODUCT, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php index ebbb1775aa7f8..0acd7ef315700 100644 --- a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php +++ b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php @@ -14,6 +14,8 @@ /** * Plugin model for Catalog Resource Attribute + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class EavAttribute { @@ -29,6 +31,11 @@ class EavAttribute */ const BASE_OPTION_TITLE = 'option'; + /** + * Prefix added to option value added through API + */ + private const API_OPTION_PREFIX = 'id_'; + /** * @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory */ @@ -189,7 +196,9 @@ protected function processSwatchOptions(Attribute $attribute) if (!empty($optionsArray) && is_array($optionsArray)) { $optionsArray = $this->prepareOptionIds($optionsArray); - $attributeSavedOptions = $attribute->getSource()->getAllOptions(); + $adminStoreAttribute = clone $attribute; + $adminStoreAttribute->setStoreId(self::DEFAULT_STORE_ID); + $attributeSavedOptions = $adminStoreAttribute->getSource()->getAllOptions(); $this->prepareOptionLinks($optionsArray, $attributeSavedOptions); } @@ -227,10 +236,9 @@ protected function prepareOptionLinks(array $optionsArray, array $attributeSaved { $dependencyArray = []; if (is_array($optionsArray['value'])) { - $optionCounter = 1; - foreach (array_keys($optionsArray['value']) as $baseOptionId) { - $dependencyArray[$baseOptionId] = $attributeSavedOptions[$optionCounter]['value']; - $optionCounter++; + $options = array_column($attributeSavedOptions, 'value', 'label'); + foreach ($optionsArray['value'] as $id => $labels) { + $dependencyArray[$id] = $options[$labels[self::DEFAULT_STORE_ID]]; } } @@ -285,7 +293,7 @@ protected function processVisualSwatch(Attribute $attribute) * Clean swatch option values after switching to the dropdown type. * * @param array $attributeOptions - * @param int|null $swatchType + * @param int|null $swatchType * @throws \Magento\Framework\Exception\LocalizedException */ private function cleanEavAttributeOptionSwatchValues(array $attributeOptions, int $swatchType = null) @@ -309,6 +317,8 @@ private function cleanTextSwatchValuesAfterSwitch(array $attributeOptions) } /** + * Get the visual swatch type based on its value + * * @param string $value * @return int */ @@ -368,7 +378,7 @@ protected function processTextualSwatch(Attribute $attribute) */ protected function getAttributeOptionId($optionId) { - if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE) { + if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE || substr($optionId, 0, 3) == self::API_OPTION_PREFIX) { $optionId = isset($this->dependencyArray[$optionId]) ? $this->dependencyArray[$optionId] : null; } return $optionId; @@ -447,13 +457,10 @@ protected function saveDefaultSwatchOptionValue(Attribute $attribute) if (!empty($defaultValue)) { /** @var \Magento\Swatches\Model\Swatch $swatch */ $swatch = $this->swatchFactory->create(); - // created and removed on frontend option not exists in dependency array - if (substr($defaultValue, 0, 6) == self::BASE_OPTION_TITLE && - isset($this->dependencyArray[$defaultValue]) - ) { - $defaultValue = $this->dependencyArray[$defaultValue]; - } - $swatch->getResource()->saveDefaultSwatchOption($attribute->getId(), $defaultValue); + $swatch->getResource()->saveDefaultSwatchOption( + $attribute->getId(), + $this->getAttributeOptionId($defaultValue) + ); } } @@ -503,6 +510,10 @@ protected function isOptionsValid(array $options, Attribute $attribute) } /** + * Modifies Attribute::usesSource() response + * + * Returns true if attribute type is swatch + * * @param Attribute $attribute * @param bool $result * @return bool diff --git a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php new file mode 100644 index 0000000000000..795c48f12ebcc --- /dev/null +++ b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Swatches\Plugin\Eav\Model\Entity\Attribute; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Store\Model\Store; +use Magento\Swatches\Helper\Data; + +/** + * OptionManagement Plugin + */ +class OptionManagement +{ + /** + * @var AttributeRepository + */ + private $attributeRepository; + /** + * @var Data + */ + private $swatchHelper; + + /** + * @param AttributeRepository $attributeRepository + * @param Data $swatchHelper + */ + public function __construct( + AttributeRepository $attributeRepository, + Data $swatchHelper + ) { + $this->attributeRepository = $attributeRepository; + $this->swatchHelper = $swatchHelper; + } + + /** + * Add swatch value to the attribute option + * + * @param \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject + * @param string $attributeCode + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeAdd( + \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject, + ?string $attributeCode, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ) { + if (empty($attributeCode)) { + return; + } + $attribute = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode + ); + if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + return; + } + $optionId = $this->getOptionId($option); + $optionsValue = $option->getValue(); + if ($this->swatchHelper->isVisualSwatch($attribute)) { + $attribute->setData('swatchvisual', ['value' => [$optionId => $optionsValue]]); + } else { + $options = []; + $options['value'][$optionId][Store::DEFAULT_STORE_ID] = $optionsValue; + if (is_array($option->getStoreLabels())) { + foreach ($option->getStoreLabels() as $label) { + if (!isset($options['value'][$optionId][$label->getStoreId()])) { + $options['value'][$optionId][$label->getStoreId()] = null; + } + } + } + $attribute->setData('swatchtext', $options); + } + } + + /** + * Returns option id + * + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return string + */ + private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + { + return 'id_' . ($option->getValue() ?: 'new_option'); + } +} diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml index 8b95b86065b7d..4a67c0dfbe8e4 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -85,6 +85,15 @@ <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" stepKey="switchOnUsedInProductListing" userInput="Yes" after="switchOnVisibleOnCatalogPagesOnStorefront"/> </actionGroup> + <actionGroup name="AddVisualSwatchWithProductWithStorefrontPreviewImageConfigActionGroup" extends="AddVisualSwatchToProductActionGroup"> + <selectOption selector="{{AdminNewAttributePanel.updateProductPreviewImage}}" userInput="Yes" stepKey="selectUpdatePreviewImage" after="selectInputType"/> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillDefaultStoreLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" after="goToStorefrontPropertiesTab" stepKey="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" userInput="Filterable (with results)" stepKey="selectUseInLayer" after="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" userInput="Yes" stepKey="switchOnUsedInProductListing" after="selectUseInLayer"/> + <selectOption selector="{{AdminNewAttributePanel.usedForStoringInProductListing}}" userInput="Yes" stepKey="switchOnUsedForStoringInProductListing" after="switchOnUsedInProductListing"/> + </actionGroup> + <actionGroup name="AddTextSwatchToProductActionGroup"> <annotations> <description>Add text swatch property attribute.</description> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml index 0c2dea5f41235..5149fa3a1e518 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewAttributePanel"> + <element name="updateProductPreviewImage" type="select" selector="#update_product_preview_image"/> <element name="addVisualSwatchOption" type="button" selector="button#add_new_swatch_visual_option_button"/> <element name="addTextSwatchOption" type="button" selector="button#add_new_swatch_text_option_button"/> <element name="visualSwatchOptionAdminValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][0]']" parameterized="true"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 6fdf2276f39d9..5b714f01fd46f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,5 +16,6 @@ <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> <element name="visualSwatchOption" type="button" selector=".swatch-option[option-tooltip-value='#{{visualSwatchOption}}']" parameterized="true"/> + <element name="swatchOptionTooltip" type="block" selector="div.swatch-option-tooltip"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml index 3ef347b7aca12..87d3f0bb5bcb9 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a new product attribute of type "Text Swatch" --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml index 90e94466351b6..65f0e2b09b82a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml @@ -35,7 +35,7 @@ <waitForPageLoad stepKey="waitToClickSave"/> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit"/> <!-- Logout --> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Go to the edit page for the "color" attribute --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml new file mode 100644 index 0000000000000..3d69895b0c895 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDisablingSwatchTooltipsTest"> + <annotations> + <features value="Swatches"/> + <title value="Admin disabling swatch tooltips test."/> + <description value="Verify possibility to disable/enable swatch tooltips."/> + <severity value="AVERAGE"/> + <group value="Swatches"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Log in --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Clean up our modifications to the existing color attribute --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" + stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + <click selector="{{AdminManageSwatchSection.nthDelete('1')}}" stepKey="deleteSwatch1"/> + <waitForPageLoad stepKey="waitToClickSave"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logOut"/> + + <!-- Delete category --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + + <!-- Enable swatch tooltips --> + <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 1" stepKey="disableTooltips"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnabling"/> + </after> + + <!-- Go to the edit page for the "color" attribute --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" + stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + + <!-- Change to visual swatches --> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="swatch_visual" + stepKey="selectVisualSwatch"/> + + <!-- Set swatch using the color picker --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch1"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch1"> + <argument name="index" value="0"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('1')}}" stepKey="clickChooseColor1"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillHex1"> + <argument name="nthColorPicker" value="1"/> + <argument name="hexColor" value="e74c3c"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="red" stepKey="fillAdmin1"/> + <waitForPageLoad stepKey="waitToClickSave"/> + + <!-- Save --> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit1"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> + + <!-- Assert that the Save was successful after round trip to server --> + <actionGroup ref="assertSwatchColor" stepKey="assertSwatchAdmin"> + <argument name="nthSwatch" value="1"/> + <argument name="expectedStyle" value="background: rgb(231, 77, 60);"/> + </actionGroup> + + <!-- Create a configurable product to verify the storefront with --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" + stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" + stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" + stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" + stepKey="fillPrice"/> + <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" + stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" + parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" + stepKey="fillUrlKey"/> + + <!-- Create configurations based on the Swatch we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" + stepKey="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="color" + stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesByAttributeToEachSku}}" + stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" userInput="Color" + stepKey="selectAttributes"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attribute1}}" userInput="10" + stepKey="fillAttributePrice1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" + stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="99" + stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" + dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" + stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + <seeInTitle userInput="{{_defaultProduct.name}}" stepKey="seeProductNameInTitle"/> + + <!-- Go to the product page and see swatch options --> + <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!-- Verify that the storefront shows the swatches too --> + <actionGroup ref="assertStorefrontSwatchColor" stepKey="assertSwatchStorefront"> + <argument name="nthSwatch" value="1"/> + <argument name="expectedRgb" value="rgb(231, 77, 60)"/> + </actionGroup> + + <!-- Verify swatch tooltips are visible--> + <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverEnabledSwatch"/> + <wait time="1" stepKey="waitForTooltip1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipVisible"/> + + <!-- Disable swatch tooltips --> + <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterDisabling"/> + + <!-- Verify swatch tooltips are not visible --> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReload"/> + <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverDisabledSwatch"/> + <wait time="1" stepKey="waitForTooltip2"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipNotVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml index 470421776cf8f..1bcdd6fcf9a3a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml @@ -29,6 +29,9 @@ <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('3')}}" userInput="123456789012345678901" stepKey="fillSwatch3" after="clickAddSwatch3"/> <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('3')}}" userInput="123456789012345678901BrownD" stepKey="fillDescription3" after="fillSwatch3"/> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" userInput="123456789012345678901" stepKey="seeGreen" after="seeBlue"/> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" userInput="123456789012345678901" stepKey="seeBrown" after="seeGreen"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index b1ae06428c0ab..c9602ddcd127c 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -30,7 +30,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -103,6 +103,9 @@ <argument name="image" value="TestImageAdobe"/> </actionGroup> + <!-- Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 28df5ffd53436..7bf63d25417e3 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -28,7 +28,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -82,6 +82,9 @@ <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> </actionGroup> + <!--Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index d12cb0433fed1..fd38c48919416 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -30,7 +30,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Begin creating a new product attribute --> @@ -94,6 +94,9 @@ <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> </actionGroup> + <!-- Run re-index task--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml new file mode 100644 index 0000000000000..2928011662864 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontImageColorWhenFilterByColorFilterTest"> + <annotations> + <features value="Swatches"/> + <stories value="Color image when filtering by color filter"/> + <title value="Image color when filtering by color filter on the Storefront"/> + <description value="Image color when filtering by color filter on the Storefront"/> + <severity value="MAJOR"/> + <useCaseId value="MC-18821"/> + <testCaseId value="MC-11531"/> + <group value="Swatches"/> + </annotations> + <before> + <!--Create category and configurable product with two options--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="adminDataGridSelectPerPage" stepKey="selectNumberOfProductsPerPage"> + <argument name="perPage" value="100"/> + </actionGroup> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteAllProducts"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="navigateToConfigProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!--Create visual swatch attribute--> + <actionGroup ref="AddVisualSwatchWithProductWithStorefrontPreviewImageConfigActionGroup" stepKey="addSwatchToProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + <argument name="option1" value="visualSwatchOption1"/> + <argument name="option2" value="visualSwatchOption2"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> + <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <!--Add images to product attribute options--> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="MagentoLogo"/> + <argument name="frontend_label" value="{{visualSwatchAttribute.default_label}}"/> + <argument name="label" value="{{visualSwatchOption1.default_label}}"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="TestImageNew"/> + <argument name="frontend_label" value="{{visualSwatchAttribute.default_label}}"/> + <argument name="label" value="{{visualSwatchOption2.default_label}}"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnGenerateProductsButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Select any option in the Layered navigation and verify product image--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption1.default_label)}}" stepKey="waitForOption"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption1.default_label)}}" stepKey="clickFirstOption"/> + <grabAttributeFrom selector="{{StorefrontCategoryMainSection.productImage}}" userInput="src" stepKey="grabFirstOptionImg"/> + <assertContains expectedType="string" expected="{{MagentoLogo.filename}}" actualType="variable" actual="$grabFirstOptionImg" stepKey="assertProductFirstOptionImage"/> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeSideBarFilter"/> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeForSecondOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption2.default_label)}}" stepKey="waitForSecondOption"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionByLabel(visualSwatchOption2.default_label)}}" stepKey="clickSecondOption"/> + <grabAttributeFrom selector="{{StorefrontCategoryMainSection.productImage}}" userInput="src" stepKey="grabSecondOptionImg"/> + <assertContains expectedType="string" expected="{{TestImageNew.filename}}" actualType="variable" actual="$grabSecondOptionImg" stepKey="assertProductSecondOptionImage"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 7ef030ef8dfa8..5e712ebc38292 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -26,7 +26,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php index 317ea77107222..6bdb83c3a8129 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php @@ -6,87 +6,153 @@ namespace Magento\Swatches\Test\Unit\Model\Plugin; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Swatches\Helper\Data; use Magento\Swatches\Model\Plugin\EavAttribute; +use Magento\Swatches\Model\ResourceModel\Swatch\Collection; +use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory; use Magento\Swatches\Model\Swatch; +use Magento\Swatches\Model\SwatchAttributeType; +use Magento\Swatches\Model\SwatchFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class EavAttributeTest extends \PHPUnit\Framework\TestCase +/** + * Test plugin model for Catalog Resource Attribute + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) + */ +class EavAttributeTest extends TestCase { - const ATTRIBUTE_ID = 123; - const OPTION_ID = 'option 12'; - const STORE_ID = 'option 89'; - const ATTRIBUTE_DEFAULT_VALUE = 1; - const ATTRIBUTE_OPTION_VALUE = 2; - const ATTRIBUTE_SWATCH_VALUE = 3; + private const ATTRIBUTE_ID = 123; + private const OPTION_1_ID = 1; + private const OPTION_2_ID = 2; + private const ADMIN_STORE_ID = 0; + private const DEFAULT_STORE_ID = 1; + private const NEW_OPTION_KEY = 'option_2'; + private const ATTRIBUTE_DEFAULT_VALUE = [ + 0 => self::NEW_OPTION_KEY + ]; + private const VISUAL_ATTRIBUTE_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'Black', + self::DEFAULT_STORE_ID => 'Black', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'White', + self::DEFAULT_STORE_ID => 'White', + ], + ] + ]; + private const VISUAL_SWATCH_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => '#000000', + self::NEW_OPTION_KEY => '#ffffff', + ] + ]; + private const VISUAL_SAVED_OPTIONS = [ + [ + 'value' => self::OPTION_1_ID, + 'label' => 'Black', + ], + [ + 'value' => self::OPTION_2_ID, + 'label' => 'White', + ] + ]; + private const TEXT_ATTRIBUTE_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'Small', + self::DEFAULT_STORE_ID => 'Small', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'Medium', + self::DEFAULT_STORE_ID => 'Medium', + ], + ] + ]; + private const TEXT_SWATCH_OPTIONS = [ + 'value' => [ + self::OPTION_1_ID => [ + self::ADMIN_STORE_ID => 'S', + self::DEFAULT_STORE_ID => 'S', + ], + self::NEW_OPTION_KEY => [ + self::ADMIN_STORE_ID => 'M', + self::DEFAULT_STORE_ID => 'M', + ], + ] + ]; + private const TEXT_SAVED_OPTIONS = [ + [ + 'value' => self::OPTION_1_ID, + 'label' => 'Small', + ], + [ + 'value' => self::OPTION_2_ID, + 'label' => 'Medium', + ] + ]; /** @var EavAttribute */ private $eavAttribute; - /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Attribute|MockObject */ private $attribute; - /** @var \Magento\Swatches\Model\SwatchFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var SwatchFactory|MockObject */ private $swatchFactory; - /** @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var CollectionFactory|MockObject */ private $collectionFactory; - /** @var \Magento\Swatches\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Data|MockObject */ private $swatchHelper; - /** @var \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource|\PHPUnit_Framework_MockObject_MockObject */ + /** @var AbstractSource|MockObject */ private $abstractSource; - /** @var \Magento\Swatches\Model\Swatch|\PHPUnit_Framework_MockObject_MockObject */ - private $swatch; - - /** @var \Magento\Swatches\Model\ResourceModel\Swatch|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Swatches\Model\ResourceModel\Swatch|MockObject */ private $resource; - /** @var \Magento\Swatches\Model\ResourceModel\Swatch\Collection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Collection|MockObject */ private $collection; - /** @var array */ - private $optionIds = []; - - /** @var array */ - private $allOptions = []; - - /** @var array */ - private $dependencyArray = []; - + /** + * {@inheritDoc} + */ protected function setUp() { - $this->attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); - $this->swatchFactory = $this->createPartialMock(\Magento\Swatches\Model\SwatchFactory::class, ['create']); - $this->swatchHelper = $this->createMock(\Magento\Swatches\Helper\Data::class); - $this->swatch = $this->createMock(\Magento\Swatches\Model\Swatch::class); - $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class); - $this->collection = - $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch\Collection::class); - $this->collectionFactory = $this->createPartialMock( - \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory::class, + $objectManager = new ObjectManager($this); + $this->abstractSource = $this->createMock(AbstractSource::class); + $this->attribute = $this->createPartialMock( + Attribute::class, + ['getSource'] + ); + $this->attribute->setId(self::ATTRIBUTE_ID); + $this->swatchFactory = $this->createPartialMock( + SwatchFactory::class, ['create'] ); - $this->abstractSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class); - - $serializer = $this->createPartialMock( - \Magento\Framework\Serialize\Serializer\Json::class, - ['serialize', 'unserialize'] + $this->swatchHelper = $objectManager->getObject( + Data::class, + [ + 'swatchTypeChecker' => $objectManager->getObject(SwatchAttributeType::class) + ] ); - - $serializer->expects($this->any()) - ->method('serialize')->willReturnCallback(function ($parameter) { - return json_encode($parameter); - }); - - $serializer->expects($this->any()) - ->method('unserialize')->willReturnCallback(function ($parameter) { - return json_decode($parameter, true); - }); - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class); + $this->collection = $this->createMock(Collection::class); + $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']); + $serializer = $objectManager->getObject(Json::class); $this->eavAttribute = $objectManager->getObject( - \Magento\Swatches\Model\Plugin\EavAttribute::class, + EavAttribute::class, [ 'collectionFactory' => $this->collectionFactory, 'swatchFactory' => $this->swatchFactory, @@ -94,220 +160,128 @@ protected function setUp() 'serializer' => $serializer, ] ); - - $this->optionIds = [ - 'value' => ['option 89' => 'test 1', 'option 114' => 'test 2', 'option 170' => 'test 3'], - 'delete' => ['option 89' => 0, 'option 114' => 1, 'option 170' => 0], - ]; - $this->allOptions = [null, ['value' => 'option 12'], ['value' => 'option 154']]; - $this->dependencyArray = ['option 89', 'option 170']; + $this->attribute->expects($this->any()) + ->method('getSource') + ->willReturn($this->abstractSource); + $swatch = $this->createMock(Swatch::class); + $swatch->expects($this->any()) + ->method('getResource') + ->willReturn($this->resource); + $this->swatchFactory->expects($this->any()) + ->method('create') + ->willReturn($swatch); } + /** + * Test beforeSave plugin for visual swatch + */ public function testBeforeSaveVisualSwatch() { - $option = [ - 'value' => [ - 0 => 'option value', + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'] - )->will($this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $option, - false - )); - - $this->attribute->expects($this->exactly(3))->method('setData') - ->withConsecutive( - ['option', self::ATTRIBUTE_OPTION_VALUE], - ['default', self::ATTRIBUTE_DEFAULT_VALUE], - ['swatch', self::ATTRIBUTE_SWATCH_VALUE] - ); - - $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute') - ->with($this->attribute); - $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals(self::VISUAL_ATTRIBUTE_OPTIONS, $this->attribute->getData('option')); + $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } + /** + * Test beforeSave plugin for text swatch + */ public function testBeforeSaveTextSwatch() { - $option = [ - 'value' => [ - 0 => 'option value', + $this->attribute->setData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, ] - ]; - $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive( - ['defaulttext'], - ['optiontext'], - ['swatchtext'], - ['optiontext'], - ['option/delete/0'] - )->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $option, - false - ) ); - $this->attribute->expects($this->exactly(3))->method('setData') - ->withConsecutive( - ['option', self::ATTRIBUTE_OPTION_VALUE], - ['default', self::ATTRIBUTE_DEFAULT_VALUE], - ['swatch', self::ATTRIBUTE_SWATCH_VALUE] - ); - - $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute') - ->with($this->attribute); - $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->atLeastOnce())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals(self::TEXT_ATTRIBUTE_OPTIONS, $this->attribute->getData('option')); + $this->assertEquals(self::TEXT_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } /** + * Test beforeSave plugin on empty label + * * @expectedException \Magento\Framework\Exception\InputException * @expectedExceptionMessage Admin is a required field in each row */ public function testBeforeSaveWithFailedValidation() { - $optionText = [ - 'value' => [ - 0 => '', + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = ''; + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - $this->swatchHelper->expects($this->once()) - ->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - - $this->swatchHelper->expects($this->atLeastOnce()) - ->method('isVisualSwatch') - ->willReturn(true); - $this->attribute->expects($this->exactly(5)) - ->method('getData') - ->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'] - ) - ->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $optionText, - false - ) - ); + ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); } /** - * @covers \Magento\Swatches\Model\Plugin\EavAttribute::beforeBeforeSave() + * Test beforeSave plugin on empty label of option being deleted */ - public function testBeforeSaveWithDeletedOption() + public function testValidationIsSkippedForDeletedOption() { - $optionText = [ - 'value' => [ - 0 => '', + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = ''; + $options['delete'][self::NEW_OPTION_KEY] = '1'; + $this->attribute->setData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, ] - ]; - - $this->swatchHelper->expects($this->once()) - ->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); + ); - $this->swatchHelper->expects($this->atLeastOnce()) - ->method('isVisualSwatch') - ->willReturn(true); - $this->attribute->expects($this->exactly(6)) - ->method('getData') - ->withConsecutive( - ['defaultvisual'], - ['optionvisual'], - ['swatchvisual'], - ['optionvisual'], - ['option/delete/0'], - ['swatch_input_type'] - ) - ->will( - $this->onConsecutiveCalls( - self::ATTRIBUTE_DEFAULT_VALUE, - self::ATTRIBUTE_OPTION_VALUE, - self::ATTRIBUTE_SWATCH_VALUE, - $optionText, - true, - false - ) - ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default')); + $this->assertEquals($options, $this->attribute->getData('option')); + $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch')); } + /** + * Test beforeSave plugin for non a swatch attribute + */ public function testBeforeSaveNotSwatch() { $additionalData = [ - 'swatch_input_type' => 'visual', - 'update_product_preview_image' => 1, - 'use_product_image_for_swatch' => 0 - ]; - - $shortAdditionalData = [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, 'update_product_preview_image' => 1, 'use_product_image_for_swatch' => 0 ]; - $this->attribute->expects($this->exactly(2))->method('getData')->withConsecutive( - [Swatch::SWATCH_INPUT_TYPE_KEY], - ['additional_data'] - )->willReturnOnConsecutiveCalls( - Swatch::SWATCH_INPUT_TYPE_DROPDOWN, - json_encode($additionalData) + $this->attribute->setData( + [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_DROPDOWN, + 'additional_data' => json_encode($additionalData), + ] ); - $this->attribute - ->expects($this->once()) - ->method('setData') - ->with('additional_data', json_encode($shortAdditionalData)) - ->will($this->returnSelf()); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_DROPDOWN); - $this->swatchHelper->expects($this->never())->method('assembleAdditionalDataEavAttribute'); - $this->swatchHelper->expects($this->never())->method('isVisualSwatch'); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->swatchHelper->expects($this->once())->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(false); + unset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY]); - $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->assertEquals(json_encode($additionalData), $this->attribute->getData('additional_data')); } /** @@ -316,390 +290,383 @@ public function testBeforeSaveNotSwatch() public function visualSwatchProvider() { return [ - [Swatch::SWATCH_TYPE_EMPTY, null], - [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#hex'], - [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path'], + [Swatch::SWATCH_TYPE_EMPTY, 'black', 'white'], + [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#000000', '#ffffff'], + [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path/black.png', '/path/white.png'], ]; } /** - * @dataProvider visualSwatchProvider + * Test afterSave plugin for visual swatch + * + * @param string $swatchType + * @param string $swatch1 + * @param string $swatch2 * - * @param $swatchType - * @param $swatchValue + * @dataProvider visualSwatchProvider */ - public function testAfterAfterSaveVisualSwatch($swatchType, $swatchValue) + public function testAfterAfterSaveVisualSwatch(string $swatchType, string $swatch1, string $swatch2) { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $options = self::VISUAL_SWATCH_OPTIONS; + $options['value'][self::OPTION_1_ID] = $swatch1; + $options['value'][self::NEW_OPTION_KEY] = $swatch2; + $this->attribute->addData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS, + 'swatchvisual' => $options, + ] + ); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->exactly(4))->method('setData') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', EavAttribute::DEFAULT_STORE_ID], - ['type', $swatchType], - ['value', $swatchValue] - ); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); + $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::VISUAL_SAVED_OPTIONS); + + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') + $this->collection->expects($this->exactly(4)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', EavAttribute::DEFAULT_STORE_ID] - )->willReturnSelf(); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID] + ) + ->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collection->expects($this->exactly(2)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + $swatchType, + $swatch1, + 1 + ), + $this->createSwatchMock( + $swatchType, + $swatch2, + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(2)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => $swatchValue]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); - $this->eavAttribute->afterAfterSave($this->attribute); } - public function testDefaultTextualSwatchAfterSave() + /** + * Test afterSave plugin for text swatch + */ + public function testAfterAfterSaveTextualSwatch() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, + ] + ); - $this->swatch->expects($this->any())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->any())->method('save'); - $this->swatch->expects($this->any())->method('isDeleted') - ->with(false); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->collection->expects($this->any())->method('addFieldToFilter') - ->willReturnSelf(); - $this->collection->expects($this->any())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->any())->method('create') - ->willReturn($this->collection); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn(null); - - $this->attribute->expects($this->at(3))->method('getData') - ->with('swatch/value') - ->willReturn( - [ - self::STORE_ID => [ - 1 => "test", - 2 => false, - 3 => null, - 4 => "", - ] - ] - ); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - - $this->swatch->expects($this->any())->method('setData') + $this->collection->expects($this->exactly(8)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', 1], - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', "test"] - ); - - $this->eavAttribute->afterAfterSave($this->attribute); - } - - public function testAfterAfterSaveTextualSwatch() - { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_1_ID], + ['store_id', self::DEFAULT_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(EavAttribute::DEFAULT_STORE_ID); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(4))->method('setData') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID], - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] + $this->collection->expects($this->exactly(4)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID], + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID], + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID], + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) ); - - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collectionFactory->expects($this->exactly(4)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin for deleted visual swatch option + */ public function testAfterAfterSaveVisualSwatchIsDelete() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $options = self::VISUAL_ATTRIBUTE_OPTIONS; + $options['delete'][self::OPTION_1_ID] = '1'; + $this->attribute->addData( + [ + 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optionvisual' => $options, + 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS, + ] + ); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL); + $this->eavAttribute->beforeBeforeSave($this->attribute); + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::VISUAL_SAVED_OPTIONS); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => null]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(true); - - $this->swatchFactory->expects($this->once())->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->never())->method('isTextSwatch'); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); + + $this->collection->expects($this->exactly(2)) + ->method('addFieldToFilter') + ->withConsecutive( + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID] + ) + ->willReturnSelf(); + + $this->collection->expects($this->exactly(1)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_VISUAL_COLOR, + self::VISUAL_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(1)) + ->method('create') + ->willReturn($this->collection); $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin for deleted text swatch option + */ public function testAfterAfterSaveTextualSwatchIsDelete() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); - - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); + $options = self::TEXT_ATTRIBUTE_OPTIONS; + $options['delete'][self::OPTION_1_ID] = '1'; + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => $options, + 'swatchtext' => self::TEXT_SWATCH_OPTIONS, + ] + ); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(true); - - $this->swatchFactory->expects($this->once())->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->eavAttribute->afterAfterSave($this->attribute); - } + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); - public function testAfterAfterSaveIsSwatchExists() - { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - $this->resource->expects($this->once())->method('saveDefaultSwatchOption') - ->with(self::ATTRIBUTE_ID, self::OPTION_ID); + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); - $this->swatch->expects($this->once())->method('getResource') - ->willReturn($this->resource); - $this->swatch->expects($this->once())->method('getId') - ->willReturn(1); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(2))->method('setData') + $this->collection->expects($this->exactly(4)) + ->method('addFieldToFilter') ->withConsecutive( - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] - ); + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') - ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') + $this->collection->expects($this->exactly(2)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID], + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID], + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(2)) + ->method('create') ->willReturn($this->collection); - $this->attribute->expects($this->at(0))->method('getData') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('default/0') - ->willReturn($this->dependencyArray[0]); - $this->attribute->expects($this->at(3))->method('getId') - ->willReturn(self::ATTRIBUTE_ID); - $this->attribute->expects($this->at(4))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(5))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchFactory->expects($this->exactly(1))->method('create') - ->willReturn($this->swatch); - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->willReturn(true); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); - $this->eavAttribute->afterAfterSave($this->attribute); } + /** + * Test afterSave plugin on empty swatch value + */ public function testAfterAfterSaveNotSwatchAttribute() { - $this->abstractSource->expects($this->once())->method('getAllOptions') - ->willReturn($this->allOptions); - - $this->swatch->expects($this->once())->method('getId') - ->willReturn(1); - $this->swatch->expects($this->once())->method('save'); - $this->swatch->expects($this->once())->method('isDeleted') - ->with(false); - $this->swatch->expects($this->exactly(2))->method('setData') - ->withConsecutive( - ['type', Swatch::SWATCH_TYPE_TEXTUAL], - ['value', null] - ); + $options = self::TEXT_SWATCH_OPTIONS; + $options['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID] = null; + $options['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID] = null; + $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = null; + $options['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID] = null; + $this->attribute->addData( + [ + 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE, + 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS, + 'swatchtext' => $options, + ] + ); + + $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT); + $this->eavAttribute->beforeBeforeSave($this->attribute); - $this->collection->expects($this->exactly(2))->method('addFieldToFilter') + $this->abstractSource->expects($this->once()) + ->method('getAllOptions') + ->willReturn(self::TEXT_SAVED_OPTIONS); + + $this->resource->expects($this->once()) + ->method('saveDefaultSwatchOption') + ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID); + + $this->collection->expects($this->exactly(8)) + ->method('addFieldToFilter') ->withConsecutive( - ['option_id', self::OPTION_ID], - ['store_id', self::OPTION_ID] - )->willReturnSelf(); - $this->collection->expects($this->once())->method('getFirstItem') - ->willReturn($this->swatch); - $this->collectionFactory->expects($this->once())->method('create') - ->willReturn($this->collection); + ['option_id', self::OPTION_1_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_1_ID], + ['store_id', self::DEFAULT_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::ADMIN_STORE_ID], + ['option_id', self::OPTION_2_ID], + ['store_id', self::DEFAULT_STORE_ID] + ) + ->willReturnSelf(); - $this->attribute->expects($this->at(0))->method('getData') - ->with('option') - ->willReturn($this->optionIds); - $this->attribute->expects($this->at(1))->method('getSource') - ->willReturn($this->abstractSource); - $this->attribute->expects($this->at(2))->method('getData') - ->with('swatch/value') - ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]); - $this->attribute->expects($this->at(3))->method('getData') - ->with('option/delete/' . self::OPTION_ID) - ->willReturn(false); - - $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute') - ->with($this->attribute) - ->will($this->onConsecutiveCalls(true, false)); - $this->swatchHelper->expects($this->once())->method('isVisualSwatch') - ->with($this->attribute) - ->willReturn(false); - $this->swatchHelper->expects($this->once())->method('isTextSwatch') - ->with($this->attribute) - ->willReturn(true); + $this->collection->expects($this->exactly(4)) + ->method('getFirstItem') + ->willReturnOnConsecutiveCalls( + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + 1 + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + null, + self::OPTION_2_ID, + self::ADMIN_STORE_ID + ), + $this->createSwatchMock( + Swatch::SWATCH_TYPE_TEXTUAL, + null, + null, + self::OPTION_2_ID, + self::DEFAULT_STORE_ID + ) + ); + $this->collectionFactory->expects($this->exactly(4)) + ->method('create') + ->willReturn($this->collection); $this->eavAttribute->afterAfterSave($this->attribute); } + + /** + * Create configured mock for swatch model + * + * @param string $type + * @param string|null $value + * @param int|null $id + * @param int|null $optionId + * @param int|null $storeId + * @return MockObject + */ + private function createSwatchMock( + string $type, + ?string $value, + ?int $id = null, + ?int $optionId = null, + ?int $storeId = null + ) { + $swatch = $this->createMock(Swatch::class); + $swatch->expects($this->any()) + ->method('getId') + ->willReturn($id); + $swatch->expects($this->any()) + ->method('getResource') + ->willReturn($this->resource); + $swatch->expects($this->once()) + ->method('save'); + if ($id) { + $swatch->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['type', $type], + ['value', $value] + ); + } else { + $swatch->expects($this->exactly(4)) + ->method('setData') + ->withConsecutive( + ['option_id', $optionId], + ['store_id', $storeId], + ['type', $type], + ['value', $value] + ); + } + return $swatch; + } } diff --git a/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php new file mode 100644 index 0000000000000..849d79cc58d92 --- /dev/null +++ b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); +namespace Magento\Swatches\ViewModel\Product\Renderer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Class Configurable + */ +class Configurable implements ArgumentInterface +{ + /** + * Config path if swatch tooltips are enabled + */ + private const XML_PATH_SHOW_SWATCH_TOOLTIP = 'catalog/frontend/show_swatch_tooltip'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Configurable constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get config if swatch tooltips should be rendered. + * + * @return string + */ + public function getShowSwatchTooltip() + { + return $this->scopeConfig->getValue( + self::XML_PATH_SHOW_SWATCH_TOOLTIP, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Swatches/etc/adminhtml/system.xml b/app/code/Magento/Swatches/etc/adminhtml/system.xml index 2cf40ae83cc3b..6fbf110fadcd3 100644 --- a/app/code/Magento/Swatches/etc/adminhtml/system.xml +++ b/app/code/Magento/Swatches/etc/adminhtml/system.xml @@ -17,6 +17,10 @@ <label>Show Swatches in Product List</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="show_swatch_tooltip" translate="label" type="select" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Show Swatch Tooltip</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> </section> </system> diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml index 65b36558c2796..4140acc4974d6 100644 --- a/app/code/Magento/Swatches/etc/config.xml +++ b/app/code/Magento/Swatches/etc/config.xml @@ -11,6 +11,7 @@ <frontend> <swatches_per_product>16</swatches_per_product> <show_swatches_in_product_list>1</show_swatches_in_product_list> + <show_swatch_tooltip>1</show_swatch_tooltip> </frontend> </catalog> <general> diff --git a/app/code/Magento/Swatches/etc/di.xml b/app/code/Magento/Swatches/etc/di.xml index 5292bfafb6a0f..585cef924e928 100644 --- a/app/code/Magento/Swatches/etc/di.xml +++ b/app/code/Magento/Swatches/etc/di.xml @@ -81,4 +81,7 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product\Attribute\OptionManagement"> + <plugin name="swatches_product_attribute_optionmanagement_plugin" type="Magento\Swatches\Plugin\Eav\Model\Entity\Attribute\OptionManagement"/> + </type> </config> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml index c2dc36e83950c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml @@ -5,10 +5,19 @@ * See COPYING.txt for license details. */ --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list" /> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml index 6188f3957a11d..98346d6ae7e67 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml @@ -5,11 +5,18 @@ * See COPYING.txt for license details. */ --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="product.info.options.configurable" remove="true"/> <referenceBlock name="product.info.options.wrapper"> - <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" as="swatch_options" before="-" /> + <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" + as="swatch_options" before="-"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml index 91798cbd9947f..ce31f588c6c8c 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml @@ -3,10 +3,19 @@ ~ See COPYING.txt for license details. --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.widget.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> -</page> \ No newline at end of file +</page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml index c2dc36e83950c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml @@ -5,10 +5,19 @@ * See COPYING.txt for license details. */ --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list" /> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml index 9285d34efcd4c..86031a189d798 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml @@ -5,10 +5,19 @@ * See COPYING.txt for license details. */ --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="category.product.type.details.renderers"> - <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" + name="category.product.type.details.renderers.configurable" as="configurable" + template="Magento_Swatches::product/listing/renderer.phtml" + ifconfig="catalog/frontend/show_swatches_in_product_list"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml index 9982eb98d84da..c8159f1a43fe3 100644 --- a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml +++ b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml @@ -5,11 +5,18 @@ * See COPYING.txt for license details. */ --> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="product.info.options.configurable" remove="true"/> <referenceBlock name="product.info.options.wrapper"> - <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" as="swatch_options" before="-" /> + <block class="Magento\Swatches\Block\Product\Renderer\Configurable" name="product.info.options.swatches" + as="swatch_options" before="-"> + <arguments> + <argument name="configurable_view_model" + xsi:type="object">Magento\Swatches\ViewModel\Product\Renderer\Configurable</argument> + </arguments> + </block> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml index 777277a15d8cd..5838ba9625c6a 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml @@ -7,6 +7,8 @@ <?php /** @var $block \Magento\Swatches\Block\Product\Renderer\Listing\Configurable */ $productId = $block->getProduct()->getId(); +/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */ +$configurableViewModel = $block->getConfigurableViewModel() ?> <div class="swatch-opt-<?= $block->escapeHtmlAttr($productId) ?>" data-role="swatch-option-<?= $block->escapeHtmlAttr($productId) ?>"></div> @@ -22,7 +24,8 @@ $productId = $block->getProduct()->getId(); "jsonConfig": <?= /* @noEscape */ $block->getJsonConfig() ?>, "jsonSwatchConfig": <?= /* @noEscape */ $block->getJsonSwatchConfig() ?>, "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", - "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, + "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> } } } @@ -39,4 +42,4 @@ $productId = $block->getProduct()->getId(); } } } -</script> \ No newline at end of file +</script> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml index c85a6908413b5..bfabd5f3ab38f 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml @@ -4,7 +4,11 @@ * See COPYING.txt for license details. */ ?> -<?php /** @var $block \Magento\Swatches\Block\Product\Renderer\Configurable */ ?> +<?php +/** @var $block \Magento\Swatches\Block\Product\Renderer\Configurable */ +/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */ +$configurableViewModel = $block->getConfigurableViewModel() +?> <div class="swatch-opt" data-role="swatch-options"></div> <script type="text/x-magento-init"> @@ -15,7 +19,8 @@ "jsonSwatchConfig": <?= /* @noEscape */ $swatchOptions = $block->getJsonSwatchConfig() ?>, "mediaCallback": "<?= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>", "gallerySwitchStrategy": "<?= $block->escapeJs($block->getVar('gallery_switch_strategy', 'Magento_ConfigurableProduct')) ?: 'replace'; ?>", - "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>, + "showTooltip": <?= $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?> } }, "*" : { diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index f78dcc7a915ce..bd8994743976c 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -385,7 +385,8 @@ define([ var $widget = this, container = this.element, classes = this.options.classes, - chooseText = this.options.jsonConfig.chooseText; + chooseText = this.options.jsonConfig.chooseText, + showTooltip = this.options.showTooltip; $widget.optionsMap = {}; @@ -452,10 +453,12 @@ define([ }); }); - // Connect Tooltip - container - .find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]') - .SwatchRendererTooltip(); + if (showTooltip === 1) { + // Connect Tooltip + container + .find('[option-type="1"], [option-type="2"], [option-type="0"], [option-type="3"]') + .SwatchRendererTooltip(); + } // Hide all elements below more button $('.' + classes.moreButton).nextAll().hide(); @@ -1262,7 +1265,10 @@ define([ dataMergeStrategy: this.options.gallerySwitchStrategy }); } - gallery.first(); + + if (gallery) { + gallery.first(); + } } else if (justAnImage && justAnImage.img) { context.find('.product-image-photo').attr('src', justAnImage.img); } diff --git a/app/code/Magento/Tax/Model/TaxClassSearchResults.php b/app/code/Magento/Tax/Model/TaxClassSearchResults.php new file mode 100644 index 0000000000000..4e92fd10a3dec --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxClassSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxClassSearchResultsInterface; + +/** + * Service Data Object with Tax Class search results. + */ +class TaxClassSearchResults extends SearchResults implements TaxClassSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/Model/TaxRateSearchResults.php b/app/code/Magento/Tax/Model/TaxRateSearchResults.php new file mode 100644 index 0000000000000..80e9b5eaa72fa --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxRateSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxRateSearchResultsInterface; + +/** + * Service Data Object with Tax Rate search results. + */ +class TaxRateSearchResults extends SearchResults implements TaxRateSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/Model/TaxRuleSearchResults.php b/app/code/Magento/Tax/Model/TaxRuleSearchResults.php new file mode 100644 index 0000000000000..aa70b31ab22aa --- /dev/null +++ b/app/code/Magento/Tax/Model/TaxRuleSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tax\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Tax\Api\Data\TaxRuleSearchResultsInterface; + +/** + * Service Data Object with Tax Rule search results. + */ +class TaxRuleSearchResults extends SearchResults implements TaxRuleSearchResultsInterface +{ +} diff --git a/app/code/Magento/Tax/etc/db_schema.xml b/app/code/Magento/Tax/etc/db_schema.xml index f5227a9ef3a66..1fe1a1fe33d8a 100644 --- a/app/code/Magento/Tax/etc/db_schema.xml +++ b/app/code/Magento/Tax/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="tax_class" resource="default" engine="innodb" comment="Tax Class"> <column xsi:type="smallint" name="class_id" padding="6" unsigned="false" nullable="false" identity="true" - comment="Class Id"/> + comment="Class ID"/> <column xsi:type="varchar" name="class_name" nullable="false" length="255" comment="Class Name"/> <column xsi:type="varchar" name="class_type" nullable="false" length="8" default="CUSTOMER" comment="Class Type"/> @@ -19,7 +19,7 @@ </table> <table name="tax_calculation_rule" resource="default" engine="innodb" comment="Tax Calculation Rule"> <column xsi:type="int" name="tax_calculation_rule_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rule Id"/> + identity="true" comment="Tax Calculation Rule ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="int" name="priority" padding="11" unsigned="false" nullable="false" identity="false" comment="Priority"/> @@ -40,10 +40,10 @@ </table> <table name="tax_calculation_rate" resource="default" engine="innodb" comment="Tax Calculation Rate"> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rate Id"/> - <column xsi:type="varchar" name="tax_country_id" nullable="false" length="2" comment="Tax Country Id"/> + identity="true" comment="Tax Calculation Rate ID"/> + <column xsi:type="varchar" name="tax_country_id" nullable="false" length="2" comment="Tax Country ID"/> <column xsi:type="int" name="tax_region_id" padding="11" unsigned="false" nullable="false" identity="false" - comment="Tax Region Id"/> + comment="Tax Region ID"/> <column xsi:type="varchar" name="tax_postcode" nullable="true" length="21" comment="Tax Postcode"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="decimal" name="rate" scale="4" precision="12" unsigned="false" nullable="false" @@ -75,15 +75,15 @@ </table> <table name="tax_calculation" resource="default" engine="innodb" comment="Tax Calculation"> <column xsi:type="int" name="tax_calculation_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Tax Calculation Id"/> + comment="Tax Calculation ID"/> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rate Id"/> + identity="false" comment="Tax Calculation Rate ID"/> <column xsi:type="int" name="tax_calculation_rule_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rule Id"/> + identity="false" comment="Tax Calculation Rule ID"/> <column xsi:type="smallint" name="customer_tax_class_id" padding="6" unsigned="false" nullable="false" - identity="false" comment="Customer Tax Class Id"/> + identity="false" comment="Customer Tax Class ID"/> <column xsi:type="smallint" name="product_tax_class_id" padding="6" unsigned="false" nullable="false" - identity="false" comment="Product Tax Class Id"/> + identity="false" comment="Product Tax Class ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_id"/> </constraint> @@ -116,11 +116,11 @@ </table> <table name="tax_calculation_rate_title" resource="default" engine="innodb" comment="Tax Calculation Rate Title"> <column xsi:type="int" name="tax_calculation_rate_title_id" padding="11" unsigned="false" nullable="false" - identity="true" comment="Tax Calculation Rate Title Id"/> + identity="true" comment="Tax Calculation Rate Title ID"/> <column xsi:type="int" name="tax_calculation_rate_id" padding="11" unsigned="false" nullable="false" - identity="false" comment="Tax Calculation Rate Id"/> + identity="false" comment="Tax Calculation Rate ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_rate_title_id"/> @@ -140,10 +140,10 @@ </index> </table> <table name="tax_order_aggregated_created" resource="sales" engine="innodb" comment="Tax Order Aggregation"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="float" name="percent" unsigned="false" nullable="true" comment="Percent"/> @@ -170,10 +170,10 @@ </table> <table name="tax_order_aggregated_updated" resource="sales" engine="innodb" comment="Tax Order Aggregated Updated"> - <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="float" name="percent" unsigned="false" nullable="true" comment="Percent"/> diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 3b46b0f9e258c..a0b43df226f22 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -37,8 +37,8 @@ </argument> </arguments> </type> - <preference for="Magento\Tax\Api\Data\TaxRateSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> - <preference for="Magento\Tax\Api\Data\TaxClassSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxRateSearchResultsInterface" type="Magento\Tax\Model\TaxRateSearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxClassSearchResultsInterface" type="Magento\Tax\Model\TaxClassSearchResults" /> <preference for="Magento\Tax\Api\OrderTaxManagementInterface" type="Magento\Tax\Model\Sales\Order\TaxManagement" /> <preference for="Magento\Tax\Api\Data\OrderTaxDetailsAppliedTaxInterface" type="Magento\Tax\Model\Sales\Order\Tax" /> <preference for="Magento\Tax\Api\Data\OrderTaxDetailsInterface" type="Magento\Tax\Model\Sales\Order\Details" /> @@ -47,7 +47,7 @@ <preference for="Magento\Tax\Api\TaxClassRepositoryInterface" type="Magento\Tax\Model\TaxClass\Repository" /> <preference for="Magento\Tax\Api\Data\TaxClassInterface" type="Magento\Tax\Model\ClassModel" /> <preference for="Magento\Tax\Api\Data\TaxRuleInterface" type="Magento\Tax\Model\Calculation\Rule" /> - <preference for="Magento\Tax\Api\Data\TaxRuleSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Tax\Api\Data\TaxRuleSearchResultsInterface" type="Magento\Tax\Model\TaxRuleSearchResults" /> <preference for="Magento\Tax\Api\TaxRateManagementInterface" type="Magento\Tax\Model\TaxRateManagement" /> <preference for="Magento\Tax\Api\TaxRateRepositoryInterface" type="Magento\Tax\Model\Calculation\RateRepository" /> <preference for="Magento\Tax\Api\Data\TaxRateTitleInterface" type="Magento\Tax\Model\Calculation\Rate\Title" /> diff --git a/app/code/Magento/TaxGraphQl/etc/schema.graphqls b/app/code/Magento/TaxGraphQl/etc/schema.graphqls index 2b81983478447..d0be08fe9a1bd 100644 --- a/app/code/Magento/TaxGraphQl/etc/schema.graphqls +++ b/app/code/Magento/TaxGraphQl/etc/schema.graphqls @@ -2,5 +2,5 @@ # See COPYING.txt for license details. enum PriceAdjustmentCodesEnum { - TAX + TAX @deprecated(reason: "PriceAdjustmentCodesEnum is deprecated. Tax is included or excluded in price. Tax is not shown separtely in Catalog") } diff --git a/app/code/Magento/TaxImportExport/i18n/en_US.csv b/app/code/Magento/TaxImportExport/i18n/en_US.csv index 95f94dcfd3b2c..56815947ed1fa 100644 --- a/app/code/Magento/TaxImportExport/i18n/en_US.csv +++ b/app/code/Magento/TaxImportExport/i18n/en_US.csv @@ -18,3 +18,5 @@ Rate,Rate CSV,CSV "Excel XML","Excel XML" "Import/Export Tax Rates","Import/Export Tax Rates" +"Please select a file to import!","Please select a file to import!" + diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml index 7473612252bb2..1c6b267cd9289 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml @@ -31,7 +31,7 @@ </form> <?php endif; ?> <script> -require(['jquery', "mage/mage", "loadingPopup"], function(jQuery){ +require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], function(jQuery, uiAlert){ jQuery('#import-form').mage('form').mage('validation'); (function ($) { @@ -42,6 +42,10 @@ require(['jquery', "mage/mage", "loadingPopup"], function(jQuery){ }); $(this.form).submit(); + } else { + uiAlert({ + content: $.mage.__('Please select a file to import!') + }); } }); })(jQuery); diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index a9aaaedb726d6..8f81ace8c9047 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -247,7 +247,7 @@ private function getMime() */ private function getRelativeMediaPath(string $path): string { - return preg_replace('/\/(pub\/)?media\//', '', $path); + return preg_split('/\/(pub\/)?media\//', $path)[1] ?? preg_replace('/\/(pub\/)?media\//', '', $path); } /** diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 94a6ab0ec565e..91f176efbc7b9 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -282,4 +282,35 @@ public function testBeforeSaveWithExistingFile() $this->fileBackend->getValue() ); } + + /** + * Test for getRelativeMediaPath method. + * + * @param string $path + * @param string $filename + * @dataProvider getRelativeMediaPathDataProvider + */ + public function testGetRelativeMediaPath(string $path, string $filename) + { + $reflection = new \ReflectionClass($this->fileBackend); + $method = $reflection->getMethod('getRelativeMediaPath'); + $method->setAccessible(true); + $this->assertEquals( + $filename, + $method->invoke($this->fileBackend, $path . $filename) + ); + } + + /** + * Data provider for testGetRelativeMediaPath. + * + * @return array + */ + public function getRelativeMediaPathDataProvider(): array + { + return [ + 'Normal path' => ['pub/media/', 'filename.jpg'], + 'Complex path' => ['somepath/pub/media/', 'filename.jpg'], + ]; + } } diff --git a/app/code/Magento/Theme/composer.json b/app/code/Magento/Theme/composer.json index 37802ee6c68f6..ecc944336cd86 100644 --- a/app/code/Magento/Theme/composer.json +++ b/app/code/Magento/Theme/composer.json @@ -19,7 +19,6 @@ "magento/module-widget": "*" }, "suggest": { - "magento/module-translation": "*", "magento/module-theme-sample-data": "*", "magento/module-deploy": "*", "magento/module-directory": "*" diff --git a/app/code/Magento/Theme/etc/db_schema.xml b/app/code/Magento/Theme/etc/db_schema.xml index 7f3a3fc607947..84b7654e69160 100644 --- a/app/code/Magento/Theme/etc/db_schema.xml +++ b/app/code/Magento/Theme/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Theme identifier"/> <column xsi:type="int" name="parent_id" padding="11" unsigned="false" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="varchar" name="theme_path" nullable="true" length="255" comment="Theme Path"/> <column xsi:type="varchar" name="theme_title" nullable="false" length="255" comment="Theme Title"/> <column xsi:type="varchar" name="preview_image" nullable="true" length="255" comment="Preview Image"/> @@ -28,7 +28,7 @@ <column xsi:type="int" name="theme_files_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Theme files identifier"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme Id"/> + comment="Theme ID"/> <column xsi:type="varchar" name="file_path" nullable="true" length="255" comment="Relative path to file"/> <column xsi:type="varchar" name="file_type" nullable="false" length="32" comment="File Type"/> <column xsi:type="longtext" name="content" nullable="false" comment="File Content"/> @@ -43,9 +43,9 @@ </table> <table name="design_change" resource="default" engine="innodb" comment="Design Changes"> <column xsi:type="int" name="design_change_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Design Change Id"/> + comment="Design Change ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="design" nullable="true" length="255" comment="Design"/> <column xsi:type="date" name="date_from" comment="First Date of Design Activity"/> <column xsi:type="date" name="date_to" comment="Last Date of Design Activity"/> diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 07d344cb3658f..81cffe8c040b3 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -11,8 +11,6 @@ <block name="require.js" class="Magento\Framework\View\Element\Template" template="Magento_Theme::page/js/require_js.phtml" /> <referenceContainer name="after.body.start"> <block class="Magento\RequireJs\Block\Html\Head\Config" name="requirejs-config"/> - <block class="Magento\Translation\Block\Html\Head\Config" name="translate-config"/> - <block class="Magento\Translation\Block\Js" name="translate" template="Magento_Translation::translate.phtml"/> <block class="Magento\Framework\View\Element\Js\Cookie" name="js_cookies" template="Magento_Theme::js/cookie.phtml"/> <block class="Magento\Theme\Block\Html\Notices" name="global_notices" template="Magento_Theme::html/notices.phtml"/> </referenceContainer> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js index 41cc0d9813bfa..58074216458ee 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Node.js @@ -197,7 +197,7 @@ }, /** - * Wraps the node in in another node. + * Wraps the node in another node. * * @example * node.wrap(wrapperNode); diff --git a/app/code/Magento/Translation/Model/Js/DataProvider.php b/app/code/Magento/Translation/Model/Js/DataProvider.php index 7aad7c765bcd5..ae388239bc538 100644 --- a/app/code/Magento/Translation/Model/Js/DataProvider.php +++ b/app/code/Magento/Translation/Model/Js/DataProvider.php @@ -120,6 +120,8 @@ public function getData($themePath) } } + ksort($dictionary); + return $dictionary; } diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml new file mode 100644 index 0000000000000..155e174310ea9 --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontButtonsInlineTranslationTest"> + <annotations> + <features value="Translation"/> + <stories value="Inline Translation"/> + <title value="[Inline Translation] Buttons inline translation"/> + <description value="[Inline Translation] Buttons inline translation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12735"/> + <group value="translation"/> + <skip> + <issueId value="MC-20127"/> + </skip> + </annotations> + <before> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Enable Translate Inline For Storefront--> + <magentoCLI + command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" + stepKey="enableTranslateInlineForStorefront"/> + <!-- Set developer mode --> + <magentoCLI command="deploy:mode:set developer" stepKey="setDeveloperMode"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Disable Translate Inline For Storefront --> + <magentoCLI + command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" + stepKey="disableTranslateInlineForStorefront"/> + <!-- Set production mode --> + <magentoCLI command="deploy:mode:set production" stepKey="setProductionMode"/> + + <!-- Delete Simple Product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add product to cart on storefront --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Click on cart button on the top --> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="showMiniCart"/> + + <!-- Small cart popup appeared. --> + <waitForElementVisible selector="{{StorefrontMinicartSection.productName}}" stepKey="seeProductNameAppeared"/> + + <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> + <actionGroup ref="AssertElementInTranslateInlineModeActionGroup" stepKey="assertRedBordersAndBookIcon"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + + <actionGroup ref="AdminTranslateElementActionGroup" stepKey="translateProceedToCheckoutButtonText"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + <argument name="translateText" value="Proceed to Checkout Translated"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php b/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php index 021709bdda1f6..b5bfbbc29a603 100644 --- a/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php +++ b/app/code/Magento/Translation/Test/Unit/Model/Js/DataProviderTest.php @@ -90,7 +90,7 @@ public function testGetData() $themePath = 'blank'; $areaCode = 'adminhtml'; - $filePaths = [['path1'], ['path2'], ['path3'], ['path4']]; + $filePaths = [['path1'], ['path2'], ['path4'], ['path3']]; $jsFilesMap = [ ['base', $themePath, '*', '*', [$filePaths[0]]], @@ -111,8 +111,8 @@ public function testGetData() $contentsMap = [ 'content1$.mage.__("hello1")content1', 'content2$.mage.__("hello2")content2', + 'content2$.mage.__("hello4")content4', // this value should be last after running data provider 'content2$.mage.__("hello3")content3', - 'content2$.mage.__("hello4")content4' ]; $translateMap = [ @@ -147,7 +147,13 @@ public function testGetData() ->method('render') ->willReturnMap($translateMap); - $this->assertEquals($expectedResult, $this->model->getData($themePath)); + $actualResult = $this->model->getData($themePath); + $this->assertEquals($expectedResult, $actualResult); + $this->assertEquals( + json_encode($expectedResult), + json_encode($actualResult), + "Translations should be sorted by key" + ); } /** diff --git a/app/code/Magento/Translation/etc/db_schema.xml b/app/code/Magento/Translation/etc/db_schema.xml index a0d08467acf06..a8ce30a0b4fd9 100644 --- a/app/code/Magento/Translation/etc/db_schema.xml +++ b/app/code/Magento/Translation/etc/db_schema.xml @@ -9,11 +9,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="translation" resource="default" engine="innodb" comment="Translations"> <column xsi:type="int" name="key_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Key Id of Translation"/> + comment="Key ID of Translation"/> <column xsi:type="varchar" name="string" nullable="false" length="255" default="Translate String" comment="Translation String"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="varchar" name="translate" nullable="true" length="255" comment="Translate"/> <column xsi:type="varchar" name="locale" nullable="false" length="20" default="en_US" comment="Locale"/> <column xsi:type="bigint" name="crc_string" padding="20" unsigned="false" nullable="false" identity="false" diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index 445c3e88830a6..4c257eb76843f 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -6,6 +6,10 @@ /** @var \Magento\Translation\Block\Js $block */ ?> +<!-- + For frontend area dictionary file is inserted into html head in Magento/Translation/view/base/templates/dictionary.phtml + Same translation mechanism should be introduced for admin area in 2.4 version. +--> <?php if ($block->dictionaryEnabled()) : ?> <script> require.config({ diff --git a/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js b/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js new file mode 100644 index 0000000000000..72f497dde9ad8 --- /dev/null +++ b/app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'text!js-translation.json' +], function (dict) { + 'use strict'; + + return JSON.parse(dict); +}); diff --git a/app/code/Magento/Translation/view/frontend/layout/default.xml b/app/code/Magento/Translation/view/frontend/layout/default.xml new file mode 100644 index 0000000000000..244c0464301de --- /dev/null +++ b/app/code/Magento/Translation/view/frontend/layout/default.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="head.additional"> + <block class="Magento\Translation\Block\Html\Head\Config" name="translate-config"/> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index b4b3ce0f8c554..b5351b9d471cf 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -8,10 +8,12 @@ var config = { '*': { editTrigger: 'mage/edit-trigger', addClass: 'Magento_Translation/js/add-class', - 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' + 'Magento_Translation/add-class': 'Magento_Translation/js/add-class', + mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' } }, deps: [ - 'mage/translate-inline' + 'mage/translate-inline', + 'mageTranslationDictionary' ] }; diff --git a/app/code/Magento/Ui/Component/Control/SplitButton.php b/app/code/Magento/Ui/Component/Control/SplitButton.php index ef57268566ba8..5c9d09565fc66 100644 --- a/app/code/Magento/Ui/Component/Control/SplitButton.php +++ b/app/code/Magento/Ui/Component/Control/SplitButton.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Component\Control; /** @@ -22,7 +23,7 @@ class SplitButton extends Button { /** - * {@inheritdoc} + * @inheritdoc */ protected function getTemplatePath() { @@ -83,12 +84,12 @@ public function getButtonAttributesHtml() 'style' => $this->getStyle(), ]; - if (($idHard = $this->getIdHard())) { + if ($idHard = $this->getIdHard()) { $attributes['id'] = $idHard; } //TODO perhaps we need to skip data-mage-init when disabled="disabled" - if (($dataAttribute = $this->getDataAttribute())) { + if ($dataAttribute = $this->getDataAttribute()) { $this->getDataAttributes($dataAttribute, $attributes); } @@ -112,7 +113,7 @@ public function getToggleAttributesHtml() $title = $this->getLabel(); } - if (($currentClass = $this->getClass())) { + if ($currentClass = $this->getClass()) { $classes[] = $currentClass; } @@ -201,12 +202,11 @@ public function hasSplit() { return $this->hasData('has_split') ? (bool)$this->getData('has_split') : true; } - /** * Add data attributes to $attributes array * * @param array $data - * @param array &$attributes + * @param array $attributes * @return void */ protected function getDataAttributes($data, &$attributes) diff --git a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php index d39d2dc3cd930..040c27d4939ef 100644 --- a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php +++ b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Component\Form\Element; use Magento\Framework\Data\Form\Element\Editor; use Magento\Framework\Data\Form; use Magento\Framework\Data\FormFactory; -use Magento\Framework\DataObject; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Wysiwyg\ConfigInterface; @@ -51,6 +51,7 @@ public function __construct( array $config = [] ) { $wysiwygConfigData = isset($config['wysiwygConfigData']) ? $config['wysiwygConfigData'] : []; + $this->form = $formFactory->create(); $wysiwygId = $context->getNamespace() . '_' . $data['name']; $this->editor = $this->form->addField( diff --git a/app/code/Magento/Ui/Model/BookmarkSearchResults.php b/app/code/Magento/Ui/Model/BookmarkSearchResults.php new file mode 100644 index 0000000000000..2171a5c0084e2 --- /dev/null +++ b/app/code/Magento/Ui/Model/BookmarkSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Ui\Api\Data\BookmarkSearchResultsInterface; + +/** + * Service Data Object with Bookmark search results. + */ +class BookmarkSearchResults extends SearchResults implements BookmarkSearchResultsInterface +{ +} diff --git a/app/code/Magento/Ui/etc/db_schema.xml b/app/code/Magento/Ui/etc/db_schema.xml index 13a384024f18a..552bd267e707a 100644 --- a/app/code/Magento/Ui/etc/db_schema.xml +++ b/app/code/Magento/Ui/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="bookmark_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Bookmark identifier"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="User Id"/> + comment="User ID"/> <column xsi:type="varchar" name="namespace" nullable="false" length="255" comment="Bookmark namespace"/> <column xsi:type="varchar" name="identifier" nullable="false" length="255" comment="Bookmark Identifier"/> <column xsi:type="smallint" name="current" padding="6" unsigned="false" nullable="false" identity="false" diff --git a/app/code/Magento/Ui/etc/di.xml b/app/code/Magento/Ui/etc/di.xml index c029e18addf73..05ace9d556fa0 100644 --- a/app/code/Magento/Ui/etc/di.xml +++ b/app/code/Magento/Ui/etc/di.xml @@ -14,7 +14,7 @@ <preference for="Magento\Framework\View\Element\UiComponent\ContextInterface" type="Magento\Framework\View\Element\UiComponent\Context" /> <preference for="Magento\Framework\View\Element\UiComponent\LayoutInterface" type="Magento\Framework\View\Layout\Generic"/> <preference for="Magento\Authorization\Model\UserContextInterface" type="Magento\Authorization\Model\CompositeUserContext"/> - <preference for="Magento\Ui\Api\Data\BookmarkSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Ui\Api\Data\BookmarkSearchResultsInterface" type="Magento\Ui\Model\BookmarkSearchResults" /> <preference for="Magento\Ui\Api\BookmarkRepositoryInterface" type="Magento\Ui\Model\ResourceModel\BookmarkRepository"/> <preference for="Magento\Ui\Api\Data\BookmarkInterface" type="Magento\Ui\Model\Bookmark"/> <preference for="Magento\Ui\Component\Wysiwyg\ConfigInterface" type="Magento\Ui\Component\Wysiwyg\Config"/> diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js b/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js index b33f0b5c72395..53580fc069c47 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/insert-listing.js @@ -155,7 +155,7 @@ define([ updateExternalValueByEditableData: function () { var updatedExtValue; - if (!this.behaviourType === 'edit' || _.isEmpty(this.editableData) || _.isEmpty(this.externalValue())) { + if (!(this.behaviourType === 'edit') || _.isEmpty(this.editableData) || _.isEmpty(this.externalValue())) { return; } diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 3402d1d1df03b..08f67955976c4 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -1067,6 +1067,16 @@ define([ return new RegExp(param).test(value); }, $.mage.__('This link is not allowed.') + ], + 'validate-dob': [ + function (value) { + if (value === '') { + return true; + } + + return moment(value).isBefore(moment()); + }, + $.mage.__('The Date of Birth should not be greater than today.') ] }, function (data) { return { diff --git a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml index 4107f17dbc18c..f330695867e7c 100644 --- a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml +++ b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml @@ -12,5 +12,29 @@ <element name="carriersUpsTab" type="button" selector="#carriers_ups-head"/> <element name="carriersUpsType" type="select" selector="#carriers_ups_type"/> <element name="selectedUpsType" type="text" selector="#carriers_ups_type option[selected]"/> + <element name="carriersUPSActive" type="input" selector="#carriers_ups_active_inherit"/> + <element name="carriersUPSTypeSystem" type="input" selector="#carriers_ups_type_inherit"/> + <element name="carriersUPSAccountLive" type="input" selector="#carriers_ups_is_account_live_inherit"/> + <element name="carriersUPSGatewayXMLUrl" type="input" selector="#carriers_ups_gateway_xml_url_inherit"/> + <element name="carriersUPSModeXML" type="input" selector="#carriers_ups_mode_xml_inherit"/> + <element name="carriersUPSOriginShipment" type="input" selector="#carriers_ups_origin_shipment_inherit"/> + <element name="carriersUPSTitle" type="input" selector="#carriers_ups_title_inherit"/> + <element name="carriersUPSNegotiatedActive" type="input" selector="#carriers_ups_negotiated_active_inherit"/> + <element name="carriersUPSIncludeTaxes" type="input" selector="#carriers_ups_include_taxes_inherit"/> + <element name="carriersUPSShipmentRequestType" type="input" selector="#carriers_ups_shipment_requesttype_inherit"/> + <element name="carriersUPSContainer" type="input" selector="#carriers_ups_container_inherit"/> + <element name="carriersUPSDestType" type="input" selector="#carriers_ups_dest_type_inherit"/> + <element name="carriersUPSTrackingXmlUrl" type="input" selector="#carriers_ups_tracking_xml_url_inherit"/> + <element name="carriersUPSUnitOfMeasure" type="input" selector="#carriers_ups_unit_of_measure_inherit"/> + <element name="carriersUPSMaxPackageWeight" type="input" selector="#carriers_ups_max_package_weight_inherit"/> + <element name="carriersUPSPickup" type="input" selector="#carriers_ups_pickup_inherit"/> + <element name="carriersUPSMinPackageWeight" type="input" selector="#carriers_ups_min_package_weight_inherit"/> + <element name="carriersUPSHandlingType" type="input" selector="#carriers_ups_handling_type_inherit"/> + <element name="carriersUPSHandlingAction" type="input" selector="#carriers_ups_handling_action_inherit"/> + <element name="carriersUPSAllowedMethods" type="input" selector="#carriers_ups_allowed_methods_inherit"/> + <element name="carriersUPSFreeMethod" type="input" selector="#carriers_ups_free_method_inherit"/> + <element name="carriersUPSSpecificErrMsg" type="input" selector="#carriers_ups_specificerrmsg_inherit"/> + <element name="carriersUPSAllowSpecific" type="input" selector="#carriers_ups_sallowspecific_inherit"/> + <element name="carriersUPSSpecificCountry" type="input" selector="#carriers_ups_specificcountry"/> </section> </sections> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..126586669afd2 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in UPS section--> + <comment userInput="Assert configuration are disabled in UPS section" stepKey="commentSeeDisabledUPSConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" dependentSelector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" visible="false" stepKey="expandUPSTab"/> + <waitForElementVisible selector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" stepKey="waitUPSTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSActive}}" userInput="disabled" stepKey="grabUPSActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSActiveDisabled" stepKey="assertUPSActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTypeSystem}}" userInput="disabled" stepKey="grabUPSTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTypeDisabled" stepKey="assertUPSTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAccountLive}}" userInput="disabled" stepKey="grabUPSAccountLiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAccountLiveDisabled" stepKey="assertUPSAccountLiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSGatewayXMLUrl}}" userInput="disabled" stepKey="grabUPSGatewayXMLUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSGatewayXMLUrlDisabled" stepKey="assertUPSGatewayXMLUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSModeXML}}" userInput="disabled" stepKey="grabUPSModeXMLDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSModeXMLDisabled" stepKey="assertUPSModeXMLDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSOriginShipment}}" userInput="disabled" stepKey="grabUPSOriginShipmentDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSOriginShipmentDisabled" stepKey="assertUPSOriginShipmentDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTitle}}" userInput="disabled" stepKey="grabUPSTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTitleDisabled" stepKey="assertUPSTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSNegotiatedActive}}" userInput="disabled" stepKey="grabUPSNegotiatedActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSNegotiatedActiveDisabled" stepKey="assertUPSNegotiatedActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSIncludeTaxes}}" userInput="disabled" stepKey="grabUPSIncludeTaxesDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSIncludeTaxesDisabled" stepKey="assertUPSIncludeTaxesDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSShipmentRequestType}}" userInput="disabled" stepKey="grabUPSShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSShipmentRequestTypeDisabled" stepKey="assertUPSShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSContainer}}" userInput="disabled" stepKey="grabUPSContainerDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSContainerDisabled" stepKey="assertUPSContainerDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSDestType}}" userInput="disabled" stepKey="grabUPSDestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSDestTypeDisabled" stepKey="assertUPSDestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTrackingXmlUrl}}" userInput="disabled" stepKey="grabUPSTrackingXmlUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSTrackingXmlUrlDisabled" stepKey="assertUPSTrackingXmlUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSUnitOfMeasure}}" userInput="disabled" stepKey="grabUPSUnitOfMeasureDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSUnitOfMeasureDisabled" stepKey="assertUPSUnitOfMeasureDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSMaxPackageWeight}}" userInput="disabled" stepKey="grabUPSMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSMaxPackageWeightDisabled" stepKey="assertUPSMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSPickup}}" userInput="disabled" stepKey="grabUPSPickupDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSPickupDisabled" stepKey="assertUPSPickupDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSMinPackageWeight}}" userInput="disabled" stepKey="grabUPSMinPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSMinPackageWeightDisabled" stepKey="assertUPSMinPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSHandlingType}}" userInput="disabled" stepKey="grabUPSHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSHandlingTypeDisabled" stepKey="assertUPSHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSHandlingAction}}" userInput="disabled" stepKey="grabUPSHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSHandlingActionDisabled" stepKey="assertUPSHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAllowedMethods}}" userInput="disabled" stepKey="grabUPSAllowedMethodsDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAllowedMethodsDisabled" stepKey="assertUPSAllowedMethodsDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSFreeMethod}}" userInput="disabled" stepKey="grabUPSFreeMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSFreeMethodDisabled" stepKey="assertUPSFreeMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSSpecificErrMsg}}" userInput="disabled" stepKey="grabUPSSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSSpecificErrMsgDisabled" stepKey="assertUPSSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAllowSpecific}}" userInput="disabled" stepKey="grabUPSAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSAllowSpecificDisabled" stepKey="assertUPSAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSSpecificCountry}}" userInput="disabled" stepKey="grabUPSSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUPSSpecificCountryDisabled" stepKey="assertUPSSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..967aa4be7cdc8 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminProductFormUpdateUrlKeyActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductFormUpdateUrlKeyActionGroup"> + <annotations> + <description>Update UrlKey for Product on Custom Store View.</description> + </annotations> + <arguments> + <argument name="newUrlKey" defaultValue="newUrlKey" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="clickOnSearchEngineOptimization"/> + <waitForLoadingMaskToDisappear stepKey="waitLoadProductForm"/> + <uncheckOption selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="uncheckDefaultUrl"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{newUrlKey}}" stepKey="changeUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.xml new file mode 100644 index 0000000000000..c97a6d9bb8f24 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontCheckProductUrlActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Check the simple product Url on the product page --> + <actionGroup name="StorefrontCheckProductUrlActionGroup"> + <annotations> + <description>Validates that the provided Simple Product Url is correct.</description> + </annotations> + <arguments> + <argument name="productUrl" type="string"/> + </arguments> + <seeInCurrentUrl url="{{StorefrontProductPage.url(productUrl)}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml new file mode 100644 index 0000000000000..c6ee1a7da9602 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminProductCreateUrlRewriteForCustomStoreViewTest"> + <annotations> + <features value="UrlRewrite"/> + <stories value="Create Product"/> + <title value="Product custom URL Key is preserved when assigned to a Category"/> + <description value="Verify Product custom URL Key (for custom Store View) is preserved when assigned to a Category (with custom URL Key) alongside with another Product without custom URL Key"/> + <testCaseId value="MC-6463"/> + <severity value="MAJOR"/> + <group value="catalog"/> + <group value="url_rewrite"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory" /> + </createData> + <createData entity="SimpleProduct" stepKey="createProductForUrlRewrite"> + <requiredEntity createDataKey="createCategory" /> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <magentoCLI command="indexer:reindex" stepKey="runReindex"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + <deleteData createDataKey="createProductForUrlRewrite" stepKey="deleteProductForUrlRewrite" /> + <deleteData createDataKey="createCategory" stepKey="deleteCategory" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearFilterForStores"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Step 1. Navigate as Admin on Product Page for edit product`s Url Key--> + <actionGroup ref="navigateToCreatedProductEditPage" stepKey="goToProductForUrlRewrite"> + <argument name="product" value="$$createProductForUrlRewrite$$"/> + </actionGroup> + <!--Step 2. As Admin switch on Custom Store View from Precondition --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToCustomStore"> + <argument name="storeView" value="customStore.name"/> + </actionGroup> + <!--Step 3. Set custom URL Key for product on Custom StoreView--> + <actionGroup ref="AdminProductFormUpdateUrlKeyActionGroup" stepKey="updateUrlKeyForProduct"> + <argument name="newUrlKey" value="U2"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductWithNewUrl"/> + <!--Step 4. Set URL Key for created category --> + <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedSubCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKey" stepKey="updateUrlKeyForCategory"> + <argument name="value" value="U1"/> + </actionGroup> + <!--Step 5. On Storefront Assert what URL Key for Category is changed and is correct as for Default Store View --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="onCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="assertUrlCategoryOnDefaultStore"> + <argument name="category" value="$$createCategory.name$$"/> + <argument name="newRequestPath" value="u1.html"/> + </actionGroup> + <!--Step 6. On Storefront Assert what URL Key for product is correct(as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductInDefaultStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductUrl"> + <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 7. On Storefront Assert what URL Key for product is correct for Default Store View (as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductForUrlRewriteInDefaultStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProductForUrlRewrite$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductWithChangedUrl"> + <argument name="productUrl" value="$$createProductForUrlRewrite.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 8. On Storefront switch on created Custom Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreViewOnStorefront"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <!--Step 9. On Storefront Assert what URL Key for Category is changed and is correct for Custom Store View --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="assertUrlCategoryOnCustomStore"> + <argument name="category" value="$$createCategory.name$$"/> + <argument name="newRequestPath" value="u1.html"/> + </actionGroup> + <!--Step 10. On Storefront Assert what URL Key for product is correct for Custom Store View (as initial URL) --> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="navigateToProductInCustomStore"> + <argument name="category" value="$$createCategory$$"/> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductUrlOnCustomStore"> + <argument name="productUrl" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!--Step 11. On Storefront Assert what URL Key for product is changed and is correct for Custom Store View --> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="assertProductUrlRewriteInStoreFront"> + <argument name="productName" value="$$createProductForUrlRewrite.name$$"/> + <argument name="productSku" value="$$createProductForUrlRewrite.sku$$"/> + <argument name="productRequestPath" value="u2.html"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml new file mode 100644 index 0000000000000..a3d3b897ef75d --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUrlRewritesForProductAfterImportTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Different number of URL rewrites when editing or importing a product"/> + <title value="Verify the number of URL rewrites when edit or import product"/> + <description value="After importing products to admin verify the number of URL including categories matches"/> + <severity value="MAJOR"/> + <testCaseId value="MC-20229"/> + <group value="urlRewrite"/> + </annotations> + <before> + <comment userInput="Set the configuration for Generate category/product URL Rewrites" stepKey="commentSetURLRewriteConfiguration" /> + <comment userInput="Enable config to generate category/product URL Rewrites " stepKey="commentEnableConfig" /> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> + <createData entity="NewRootCategory" stepKey="simpleSubCategory1"> + <field key="parent_id">2</field> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> + <requiredEntity createDataKey="simpleSubCategory1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory3"> + <requiredEntity createDataKey="simpleSubCategory2"/> + </createData> + <comment userInput="Create Simple product 1 and assign it to Category 3 " stepKey="commentCreateSimpleProduct" /> + <createData entity="SimpleProductAfterImport1" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory3"/> + </createData> + </before> + <after> + <comment userInput="Delete all products that replaced products in the before block post import " stepKey="commentDeleteAllProducts" /> + <deleteData stepKey="deleteSimpleProduct1" url="/V1/products/SimpleProductForTest1"/> + <deleteData createDataKey="simpleSubCategory3" stepKey="deleteSimpleSubCategory3"/> + <deleteData createDataKey="simpleSubCategory2" stepKey="deleteSimpleSubCategory2"/> + <deleteData createDataKey="simpleSubCategory1" stepKey="deleteSimpleSubCategory1"/> + <comment userInput="Disable config to generate category/product URL Rewrites " stepKey="commentDisableConfig" /> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + + <comment userInput="1. Log in to Admin " stepKey="commentAdminLogin" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <comment userInput="2. Open Marketing - SEO and Search - URL Rewrites " stepKey="commentVerifyUrlRewrite" /> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> + + <comment userInput="3. Import products with add/update behavior " stepKey="commentProductImport" /> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="catalog_import_products_url_rewrite.csv"/> + <argument name="importNoticeMessage" value="Created: 0, Updated: 1, Deleted: 0"/> + </actionGroup> + + <comment userInput="4. Assert Simple Product1 on grid " stepKey="commentVerifyProduct" /> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct1OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <comment userInput="5. Open Marketing - SEO and Search - URL Rewrites" stepKey="commentVerifyURLAfterImport" /> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$-new.html" stepKey="inputProductName2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue6"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$-new.html)}}" stepKey="seeInListValue7"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/etc/db_schema.xml b/app/code/Magento/UrlRewrite/etc/db_schema.xml index 6e0014873202d..06d4949e63d9a 100644 --- a/app/code/Magento/UrlRewrite/etc/db_schema.xml +++ b/app/code/Magento/UrlRewrite/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="url_rewrite" resource="default" engine="innodb" comment="Url Rewrites"> <column xsi:type="int" name="url_rewrite_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Rewrite Id"/> + comment="Rewrite ID"/> <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Entity type code"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Entity ID"/> @@ -18,7 +18,7 @@ <column xsi:type="smallint" name="redirect_type" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Redirect Type"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> + comment="Store ID"/> <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Description"/> <column xsi:type="smallint" name="is_autogenerated" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is rewrite generated automatically flag"/> diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 0acece9271f7c..e6b03755bea47 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -31,6 +31,11 @@ class EntityUrl implements ResolverInterface */ private $customUrlLocator; + /** + * @var int + */ + private $redirectType; + /** * @param UrlFinderInterface $urlFinder * @param CustomUrlLocatorInterface $customUrlLocator @@ -57,49 +62,83 @@ public function resolve( throw new GraphQlInputException(__('"url" argument should be specified and not empty')); } + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; $url = $args['url']; if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } + $this->redirectType = 0; $customUrl = $this->customUrlLocator->locateUrl($url); $url = $customUrl ?: $url; - $urlRewrite = $this->findCanonicalUrl($url, (int)$context->getExtensionAttributes()->getStore()->getId()); - if ($urlRewrite) { - if (!$urlRewrite->getEntityId()) { + $finalUrlRewrite = $this->findFinalUrl($url, $storeId); + if ($finalUrlRewrite) { + $relativeUrl = $finalUrlRewrite->getRequestPath(); + $resultArray = $this->rewriteCustomUrls($finalUrlRewrite, $storeId) ?? [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $this->redirectType, + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + + if (empty($resultArray['id'])) { throw new GraphQlNoSuchEntityException( __('No such entity found with matching URL key: %url', ['url' => $url]) ); } - $result = [ - 'id' => $urlRewrite->getEntityId(), - 'canonical_url' => $urlRewrite->getTargetPath(), - 'relative_url' => $urlRewrite->getTargetPath(), - 'type' => $this->sanitizeType($urlRewrite->getEntityType()) - ]; + + $result = $resultArray; } return $result; } /** - * Find the canonical url passing through all redirects if any + * Handle custom urls with and without redirects + * + * @param UrlRewrite $finalUrlRewrite + * @param int $storeId + * @return array|null + */ + private function rewriteCustomUrls(UrlRewrite $finalUrlRewrite, int $storeId): ?array + { + if ($finalUrlRewrite->getEntityType() === 'custom' || !($finalUrlRewrite->getEntityId() > 0)) { + $finalCustomUrlRewrite = clone $finalUrlRewrite; + $finalUrlRewrite = $this->findFinalUrl($finalCustomUrlRewrite->getTargetPath(), $storeId, true); + $relativeUrl = + $finalCustomUrlRewrite->getRedirectType() == 0 + ? $finalCustomUrlRewrite->getRequestPath() : $finalUrlRewrite->getRequestPath(); + return [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $finalCustomUrlRewrite->getRedirectType(), + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + } + return null; + } + + /** + * Find the final url passing through all redirects if any * * @param string $requestPath * @param int $storeId + * @param bool $findCustom * @return UrlRewrite|null */ - private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewrite + private function findFinalUrl(string $requestPath, int $storeId, bool $findCustom = false): ?UrlRewrite { $urlRewrite = $this->findUrlFromRequestPath($requestPath, $storeId); - if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + if ($urlRewrite) { + $this->redirectType = $urlRewrite->getRedirectType(); while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath(), $storeId); } - } - if (!$urlRewrite) { + } else { $urlRewrite = $this->findUrlFromTargetPath($requestPath, $storeId); } - if ($urlRewrite && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated()) { + if ($urlRewrite && ($findCustom && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated())) { $urlRewrite = $this->findUrlFromTargetPath($urlRewrite->getTargetPath(), $storeId); } @@ -113,7 +152,7 @@ private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewri * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?UrlRewrite + private function findUrlFromRequestPath(string $requestPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ @@ -130,7 +169,7 @@ private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?Ur * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromTargetPath(string $targetPath, int $storeId) : ?UrlRewrite + private function findUrlFromTargetPath(string $targetPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 92d237d3f01e1..7f7ebb627b4dc 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -2,13 +2,14 @@ # See COPYING.txt for license details. type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") @cache(cacheIdentity: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite\\UrlResolverIdentity") + urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page, using as input a url_key appended by the url_suffix, if one exists") @cache(cacheIdentity: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite\\UrlResolverIdentity") } type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") canonical_url: String @deprecated(reason: "The canonical_url field is deprecated, use relative_url instead.") relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + redirectCode: Int @doc(description: "301 or 302 HTTP code for url permanent or temporary redirect or 0 for the 200 no redirect") type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") } diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index d79f2013241e6..0c59f165f11f0 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -216,6 +216,9 @@ protected function _construct() * Removing dependencies and leaving only entity's properties. * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -244,6 +247,9 @@ public function __sleep() * Restoring required objects after serialization. * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { @@ -413,6 +419,10 @@ public function getRoles() */ public function getRole() { + if ($this->getData('extracted_role')) { + $this->_role = $this->getData('extracted_role'); + $this->unsetData('extracted_role'); + } if (null === $this->_role) { $this->_role = $this->_roleFactory->create(); $roles = $this->getRoles(); @@ -665,6 +675,10 @@ public function loadByUsername($username) { $data = $this->getResource()->loadByUsername($username); if ($data !== false) { + if (is_string($data['extra'])) { + $data['extra'] = $this->serializer->unserialize($data['extra']); + } + $this->setData($data); $this->setOrigData(); } diff --git a/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php b/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php deleted file mode 100644 index 23681c4b8da26..0000000000000 --- a/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\User\Test\Unit\Model\Authorization; - -use Magento\Authorization\Model\UserContextInterface; - -/** - * Tests Magento\User\Model\Authorization\AdminSessionUserContext - */ -class AdminSessionUserContextTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \Magento\User\Model\Authorization\AdminSessionUserContext - */ - protected $adminSessionUserContext; - - /** - * @var \Magento\Backend\Model\Auth\Session - */ - protected $adminSession; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->adminSession = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class) - ->disableOriginalConstructor() - ->setMethods(['hasUser', 'getUser', 'getId']) - ->getMock(); - - $this->adminSessionUserContext = $this->objectManager->getObject( - \Magento\User\Model\Authorization\AdminSessionUserContext::class, - ['adminSession' => $this->adminSession] - ); - } - - public function testGetUserIdExist() - { - $userId = 1; - - $this->setupUserId($userId); - - $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); - } - - public function testGetUserIdDoesNotExist() - { - $userId = null; - - $this->setupUserId($userId); - - $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); - } - - public function testGetUserType() - { - $this->assertEquals(UserContextInterface::USER_TYPE_ADMIN, $this->adminSessionUserContext->getUserType()); - } - - /** - * @param int|null $userId - * @return void - */ - public function setupUserId($userId) - { - $this->adminSession->expects($this->once()) - ->method('hasUser') - ->will($this->returnValue($userId)); - - if ($userId) { - $this->adminSession->expects($this->once()) - ->method('getUser') - ->will($this->returnSelf()); - - $this->adminSession->expects($this->once()) - ->method('getId') - ->will($this->returnValue($userId)); - } - } -} diff --git a/app/code/Magento/User/Test/Unit/Model/UserTest.php b/app/code/Magento/User/Test/Unit/Model/UserTest.php index 670316c2500fc..ab06c8754b2f0 100644 --- a/app/code/Magento/User/Test/Unit/Model/UserTest.php +++ b/app/code/Magento/User/Test/Unit/Model/UserTest.php @@ -44,31 +44,6 @@ protected function setUp() ); } - /** - * @return void - */ - public function testSleep() - { - $excludedProperties = [ - '_eventManager', - '_cacheManager', - '_registry', - '_appState', - '_userData', - '_config', - '_validatorObject', - '_roleFactory', - '_encryptor', - '_transportBuilder', - '_storeManager', - '_validatorBeforeSave' - ]; - $actualResult = $this->model->__sleep(); - $this->assertNotEmpty($actualResult); - $expectedResult = array_intersect($actualResult, $excludedProperties); - $this->assertEmpty($expectedResult); - } - /** * @return void */ diff --git a/app/code/Magento/User/etc/db_schema.xml b/app/code/Magento/User/etc/db_schema.xml index c3356a96b94a7..e175b50108bd9 100644 --- a/app/code/Magento/User/etc/db_schema.xml +++ b/app/code/Magento/User/etc/db_schema.xml @@ -46,9 +46,9 @@ </table> <table name="admin_passwords" resource="default" engine="innodb" comment="Admin Passwords"> <column xsi:type="int" name="password_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Password Id"/> + comment="Password ID"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="User Id"/> + comment="User ID"/> <column xsi:type="varchar" name="password_hash" nullable="true" length="100" comment="Password Hash"/> <column xsi:type="int" name="expires" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Deprecated"/> diff --git a/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml b/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml new file mode 100644 index 0000000000000..9226c40e13163 --- /dev/null +++ b/app/code/Magento/Usps/Test/Mftf/Section/AdminShippingMethodUSPSSection.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodUSPSSection"> + <element name="carriersUSPSTab" type="button" selector="#carriers_usps-head"/> + <element name="carriersUSPSActive" type="input" selector="#carriers_usps_active_inherit"/> + <element name="carriersUSPSGatewayXMLUrl" type="input" selector="#carriers_usps_gateway_url_inherit"/> + <element name="carriersUSPSGatewaySecureUrl" type="input" selector="#carriers_usps_gateway_secure_url_inherit"/> + <element name="carriersUSPSTitle" type="input" selector="#carriers_usps_title_inherit"/> + <element name="carriersUSPSUserId" type="input" selector="#carriers_usps_userid"/> + <element name="carriersUSPSPassword" type="input" selector="#carriers_usps_password"/> + <element name="carriersUSPSShipmentRequestType" type="select" selector="#carriers_usps_shipment_requesttype_inherit"/> + <element name="carriersUSPSContainer" type="input" selector="#carriers_usps_container_inherit"/> + <element name="carriersUSPSSize" type="input" selector="#carriers_usps_size_inherit"/> + <element name="carriersUSPSDestType" type="input" selector="#carriers_usps_machinable_inherit"/> + <element name="carriersUSPSMachinable" type="input" selector="#carriers_ups_tracking_xml_url_inherit"/> + <element name="carriersUSPSMaxPackageWeight" type="input" selector="#carriers_usps_max_package_weight_inherit"/> + <element name="carriersUSPSHandlingType" type="input" selector="#carriers_usps_handling_type_inherit"/> + <element name="carriersUSPSHandlingAction" type="input" selector="#carriers_usps_handling_action_inherit"/> + <element name="carriersUSPSAllowedMethods" type="input" selector="#carriers_usps_allowed_methods_inherit"/> + <element name="carriersUSPSFreeMethod" type="input" selector="#carriers_usps_free_method_inherit"/> + <element name="carriersUSPSSpecificErrMsg" type="input" selector="#carriers_usps_specificerrmsg_inherit"/> + <element name="carriersUSPSAllowSpecific" type="input" selector="#carriers_usps_sallowspecific_inherit"/> + <element name="carriersUSPSSpecificCountry" type="input" selector="#carriers_usps_specificcountry"/> + </section> +</sections> diff --git a/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml new file mode 100644 index 0000000000000..cd77861fccd58 --- /dev/null +++ b/app/code/Magento/Usps/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckInputFieldsDisabledAfterAppConfigDumpTest"> + <!--Assert configuration are disabled in USPS section--> + <comment userInput="Assert configuration are disabled in USPS section" stepKey="commentSeeDisabledUSPSConfigs"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodUSPSSection.carriersUSPSTab}}" dependentSelector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" visible="false" stepKey="expandUSPSTab"/> + <waitForElementVisible selector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" stepKey="waitUSPSTabOpen"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSActive}}" userInput="disabled" stepKey="grabUSPSActiveDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSActiveDisabled" stepKey="assertUSPSActiveDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSGatewayXMLUrl}}" userInput="disabled" stepKey="grabUSPSGatewayXMLUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSGatewayXMLUrlDisabled" stepKey="assertUSPSGatewayXMLUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSGatewaySecureUrl}}" userInput="disabled" stepKey="grabUSPSGatewaySecureUrlDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSGatewaySecureUrlDisabled" stepKey="assertUSPSGatewaySecureUrlDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSTitle}}" userInput="disabled" stepKey="grabUSPSTitleDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSTitleDisabled" stepKey="assertUSPSTitleDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSUserId}}" userInput="disabled" stepKey="grabUSPSUserIdDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSUserIdDisabled" stepKey="assertUSPSUserIdDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSPassword}}" userInput="disabled" stepKey="grabUSPSPasswordDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSPasswordDisabled" stepKey="assertUSPSPasswordDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSShipmentRequestType}}" userInput="disabled" stepKey="grabUSPSShipmentRequestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSShipmentRequestTypeDisabled" stepKey="assertUSPSShipmentRequestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSContainer}}" userInput="disabled" stepKey="grabUSPSContainerDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSContainerDisabled" stepKey="assertUSPSContainerDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSize}}" userInput="disabled" stepKey="grabUSPSSizeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSizeDisabled" stepKey="assertUSPSSizeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSDestType}}" userInput="disabled" stepKey="grabUSPSDestTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSDestTypeDisabled" stepKey="assertUSPSDestTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSMachinable}}" userInput="disabled" stepKey="grabUSPSMachinableDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSMachinableDisabled" stepKey="assertUSPSMachinableDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSMaxPackageWeight}}" userInput="disabled" stepKey="grabUSPSMaxPackageWeightDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSMaxPackageWeightDisabled" stepKey="assertUSPSMaxPackageWeightDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSHandlingType}}" userInput="disabled" stepKey="grabUSPSHandlingTypeDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSHandlingTypeDisabled" stepKey="assertUSPSHandlingTypeDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSHandlingAction}}" userInput="disabled" stepKey="grabUSPSHandlingActionDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSHandlingActionDisabled" stepKey="assertUSPSHandlingActionDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSAllowedMethods}}" userInput="disabled" stepKey="grabUSPSAllowedMethodsDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSAllowedMethodsDisabled" stepKey="assertUSPSAllowedMethodsDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSFreeMethod}}" userInput="disabled" stepKey="grabUSPSFreeMethodDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSFreeMethodDisabled" stepKey="assertUSPSFreeMethodDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSpecificErrMsg}}" userInput="disabled" stepKey="grabUSPSSpecificErrMsgDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSpecificErrMsgDisabled" stepKey="assertUSPSSpecificErrMsgDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSAllowSpecific}}" userInput="disabled" stepKey="grabUSPSAllowSpecificDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSAllowSpecificDisabled" stepKey="assertUSPSAllowSpecificDisabled"/> + <grabAttributeFrom selector="{{AdminShippingMethodUSPSSection.carriersUSPSSpecificCountry}}" userInput="disabled" stepKey="grabUSPSSpecificCountryDisabled"/> + <assertEquals expected='true' expectedType="string" actual="$grabUSPSSpecificCountryDisabled" stepKey="assertUSPSSpecificCountryDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Variable/etc/db_schema.xml b/app/code/Magento/Variable/etc/db_schema.xml index 239e3b49983c1..cd6d7d105a08a 100644 --- a/app/code/Magento/Variable/etc/db_schema.xml +++ b/app/code/Magento/Variable/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="variable" resource="default" engine="innodb" comment="Variables"> <column xsi:type="int" name="variable_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Variable Id"/> + comment="Variable ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Variable Code"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Variable Name"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -21,11 +21,11 @@ </table> <table name="variable_value" resource="default" engine="innodb" comment="Variable Value"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Variable Value Id"/> + comment="Variable Value ID"/> <column xsi:type="int" name="variable_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Variable Id"/> + default="0" comment="Variable ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="text" name="plain_value" nullable="true" comment="Plain Text Value"/> <column xsi:type="text" name="html_value" nullable="true" comment="Html Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php b/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php new file mode 100644 index 0000000000000..39dcf503779b9 --- /dev/null +++ b/app/code/Magento/Vault/Model/PaymentTokenSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Vault\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface; + +/** + * Service Data Object with Payment Token search results. + */ +class PaymentTokenSearchResults extends SearchResults implements PaymentTokenSearchResultsInterface +{ +} diff --git a/app/code/Magento/Vault/etc/db_schema.xml b/app/code/Magento/Vault/etc/db_schema.xml index 8a7c8dc4aa9fb..7110978710048 100644 --- a/app/code/Magento/Vault/etc/db_schema.xml +++ b/app/code/Magento/Vault/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> + comment="Customer ID"/> <column xsi:type="varchar" name="public_hash" nullable="false" length="128" comment="Hash code for using on frontend"/> <column xsi:type="varchar" name="payment_method_code" nullable="false" length="128" @@ -42,9 +42,9 @@ <table name="vault_payment_token_order_payment_link" resource="default" engine="innodb" comment="Order payments to vault token"> <column xsi:type="int" name="order_payment_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Order payment Id"/> + comment="Order payment ID"/> <column xsi:type="int" name="payment_token_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Payment token Id"/> + comment="Payment token ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="order_payment_id"/> <column name="payment_token_id"/> diff --git a/app/code/Magento/Vault/etc/di.xml b/app/code/Magento/Vault/etc/di.xml index 95191e4417576..0192a783bd5a8 100644 --- a/app/code/Magento/Vault/etc/di.xml +++ b/app/code/Magento/Vault/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\Vault\Api\PaymentTokenRepositoryInterface" type="Magento\Vault\Model\PaymentTokenRepository" /> <preference for="Magento\Vault\Api\PaymentTokenManagementInterface" type="Magento\Vault\Model\PaymentTokenManagement" /> <preference for="Magento\Vault\Api\PaymentMethodListInterface" type="Magento\Vault\Model\PaymentMethodList" /> - <preference for="Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface" type="Magento\Framework\Api\SearchResults" /> + <preference for="Magento\Vault\Api\Data\PaymentTokenSearchResultsInterface" type="Magento\Vault\Model\PaymentTokenSearchResults" /> <preference for="Magento\Vault\Model\Ui\TokenUiComponentInterface" type="Magento\Vault\Model\Ui\TokenUiComponent" /> <type name="Magento\Sales\Api\Data\OrderPaymentInterface"> diff --git a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php index d89513b50c9c5..8dcaabda93aab 100644 --- a/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php +++ b/app/code/Magento/Webapi/Model/Authorization/TokenUserContext.php @@ -99,7 +99,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getUserId() { @@ -108,7 +108,7 @@ public function getUserId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getUserType() { @@ -187,6 +187,8 @@ protected function processRequest() } /** + * Set user data based on user type received from token data. + * * @param Token $token * @return void */ diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index 3ddb2e441ef91..f38c0f0978536 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -33,9 +33,7 @@ class Generator extends AbstractSchemaGenerator */ const ERROR_SCHEMA = '#/definitions/error-response'; - /** - * Unauthorized description - */ + /** Unauthorized description */ const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; /** Array signifier */ @@ -759,7 +757,8 @@ private function handleComplex($name, $type, $prefix, $isArray) $subPrefix ); } - return array_merge(...$queryNames); + + return empty($queryNames) ? [] : array_merge(...$queryNames); } /** diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php b/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php index 172db875c6c49..67e361bb019d0 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php +++ b/app/code/Magento/Webapi/Test/Unit/Model/Rest/Swagger/GeneratorTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Webapi\Test\Unit\Model\Rest\Swagger; /** @@ -137,11 +138,7 @@ public function testGenerate($serviceMetadata, $typeData, $schema) ->willReturn($serviceMetadata); $this->typeProcessorMock->expects($this->any()) ->method('getTypeData') - ->willReturnMap( - [ - ['TestModule5V2EntityAllSoapAndRest', $typeData], - ] - ); + ->willReturnMap($typeData); $this->typeProcessorMock->expects($this->any()) ->method('isTypeSimple') @@ -169,6 +166,96 @@ public function testGenerate($serviceMetadata, $typeData, $schema) public function generateDataProvider() { return [ + [ + [ + 'methods' => [ + 'execute' => [ + 'method' => 'execute', + 'inputRequired' => false, + 'isSecure' => false, + 'resources' => [ + "anonymous" + ], + 'methodAlias' => 'execute', + 'parameters' => [], + 'documentation' => 'Do Magic!', + 'interface' => [ + 'in' => [ + 'parameters' => [ + 'searchRequest' => [ + 'type' => 'DreamVendorDreamModuleApiDataSearchRequestInterface', + 'required' => true, + 'documentation' => "" + ] + ] + ], + 'out' => [ + 'parameters' => [ + 'result' => [ + 'type' => 'DreamVendorDreamModuleApiDataSearchResultInterface', + 'documentation' => null, + 'required' => true + ] + ] + ] + ] + ] + ], + 'class' => 'DreamVendor\DreamModule\Api\ExecuteStuff', + 'description' => '', + 'routes' => [ + '/V1/dream-vendor/dream-module/execute-stuff' => [ + 'GET' => [ + 'method' => 'execute', + 'parameters' => [] + ] + ] + ] + ], + [ + [ + 'DreamVendorDreamModuleApiDataSearchRequestInterface', + [ + 'documentation' => '', + 'parameters' => [ + 'stuff' => [ + 'type' => 'DreamVendorDreamModuleApiDataStuffInterface', + 'required' => true, + 'documentation' => 'Empty Extension Point' + ] + ] + ] + ], + [ + 'DreamVendorDreamModuleApiDataSearchResultInterface', + [ + 'documentation' => '', + 'parameters' => [ + 'totalCount' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => 'Processed count.' + ], + 'stuff' => [ + 'type' => 'DreamVendorDreamModuleApiDataStuffInterface', + 'required' => true, + 'documentation' => 'Empty Extension Point' + ] + ] + ] + ], + [ + 'DreamVendorDreamModuleApiDataStuffInterface', + [ + 'documentation' => '', + 'parameters' => [] + ] + ] + ], + // @codingStandardsIgnoreStart + '{"swagger":"2.0","info":{"version":"","title":""},"host":"magento.host","basePath":"/rest/default","schemes":["http://"],"tags":[{"name":"testModule5AllSoapAndRestV2","description":""}],"paths":{"/V1/dream-vendor/dream-module/execute-stuff":{"get":{"tags":["testModule5AllSoapAndRestV2"],"description":"Do Magic!","operationId":"operationNameGet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"responses":{"200":{"description":"200 Success.","schema":{"$ref":"#/definitions/dream-vendor-dream-module-api-data-search-result-interface"}},"default":{"description":"Unexpected error","schema":{"$ref":"#/definitions/error-response"}}}}}},"definitions":{"error-response":{"type":"object","properties":{"message":{"type":"string","description":"Error message"},"errors":{"$ref":"#/definitions/error-errors"},"code":{"type":"integer","description":"Error code"},"parameters":{"$ref":"#/definitions/error-parameters"},"trace":{"type":"string","description":"Stack trace"}},"required":["message"]},"error-errors":{"type":"array","description":"Errors list","items":{"$ref":"#/definitions/error-errors-item"}},"error-errors-item":{"type":"object","description":"Error details","properties":{"message":{"type":"string","description":"Error message"},"parameters":{"$ref":"#/definitions/error-parameters"}}},"error-parameters":{"type":"array","description":"Error parameters list","items":{"$ref":"#/definitions/error-parameters-item"}},"error-parameters-item":{"type":"object","description":"Error parameters item","properties":{"resources":{"type":"string","description":"ACL resource"},"fieldName":{"type":"string","description":"Missing or invalid field name"},"fieldValue":{"type":"string","description":"Incorrect field value"}}},"dream-vendor-dream-module-api-data-search-result-interface":{"type":"object","description":"","properties":{"total_count":{"type":"integer","description":"Processed count."},"stuff":{"$ref":"#/definitions/dream-vendor-dream-module-api-data-stuff-interface"}},"required":["total_count","stuff"]},"dream-vendor-dream-module-api-data-stuff-interface":{"type":"object","description":""}}}' + // @codingStandardsIgnoreEnd + ], [ [ 'methods' => [ @@ -213,12 +300,17 @@ public function generateDataProvider() ], ], [ - 'documentation' => 'Some Data Object', - 'parameters' => [ - 'price' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => "" + [ + 'TestModule5V2EntityAllSoapAndRest', + [ + 'documentation' => 'Some Data Object', + 'parameters' => [ + 'price' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => "" + ] + ] ] ] ], @@ -261,12 +353,17 @@ public function generateDataProvider() ], ], [ - 'documentation' => 'Some Data Object', - 'parameters' => [ - 'price' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => "" + [ + 'TestModule5V2EntityAllSoapAndRest', + [ + 'documentation' => 'Some Data Object', + 'parameters' => [ + 'price' => [ + 'type' => 'int', + 'required' => true, + 'documentation' => "" + ] + ] ] ] ], diff --git a/app/code/Magento/Weee/Model/ResourceModel/Tax.php b/app/code/Magento/Weee/Model/ResourceModel/Tax.php index b097e4a018f22..2cbb6054a31ed 100644 --- a/app/code/Magento/Weee/Model/ResourceModel/Tax.php +++ b/app/code/Magento/Weee/Model/ResourceModel/Tax.php @@ -5,9 +5,6 @@ */ namespace Magento\Weee\Model\ResourceModel; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Condition\ConditionInterface; - /** * Wee tax resource model * @@ -21,6 +18,11 @@ class Tax extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $dateTime; + /** + * @var array + */ + private $weeeTaxCalculationsByEntityCache = []; + /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Stdlib\DateTime $dateTime @@ -46,7 +48,7 @@ protected function _construct() } /** - * Fetch one + * Fetch one calculated weee attribute from a select criteria * * @param \Magento\Framework\DB\Select|string $select * @return string @@ -57,6 +59,8 @@ public function fetchOne($select) } /** + * Is there a weee attribute available for the location provided + * * @param int $countryId * @param int $regionId * @param int $websiteId @@ -91,6 +95,8 @@ public function isWeeeInLocation($countryId, $regionId, $websiteId) } /** + * Fetch calculated weee attributes by location, store and entity + * * @param int $countryId * @param int $regionId * @param int $websiteId @@ -100,43 +106,56 @@ public function isWeeeInLocation($countryId, $regionId, $websiteId) */ public function fetchWeeeTaxCalculationsByEntity($countryId, $regionId, $websiteId, $storeId, $entityId) { - $attributeSelect = $this->getConnection()->select(); - $attributeSelect->from( - ['eavTable' => $this->getTable('eav_attribute')], - ['eavTable.attribute_code', 'eavTable.attribute_id', 'eavTable.frontend_label'] - )->joinLeft( - ['eavLabel' => $this->getTable('eav_attribute_label')], - 'eavLabel.attribute_id = eavTable.attribute_id and eavLabel.store_id = ' .((int) $storeId), - 'eavLabel.value as label_value' - )->joinInner( - ['weeeTax' => $this->getTable('weee_tax')], - 'weeeTax.attribute_id = eavTable.attribute_id', - 'weeeTax.value as weee_value' - )->where( - 'eavTable.frontend_input = ?', - 'weee' - )->where( - 'weeeTax.website_id IN(?)', - [$websiteId, 0] - )->where( - 'weeeTax.country = ?', - $countryId - )->where( - 'weeeTax.state IN(?)', - [$regionId, 0] - )->where( - 'weeeTax.entity_id = ?', - (int)$entityId + $cacheKey = sprintf( + '%s-%s-%s-%s-%s', + $countryId, + $regionId, + $websiteId, + $storeId, + $entityId ); + if (!isset($this->weeeTaxCalculationsByEntityCache[$cacheKey])) { + $attributeSelect = $this->getConnection()->select(); + $attributeSelect->from( + ['eavTable' => $this->getTable('eav_attribute')], + ['eavTable.attribute_code', 'eavTable.attribute_id', 'eavTable.frontend_label'] + )->joinLeft( + ['eavLabel' => $this->getTable('eav_attribute_label')], + 'eavLabel.attribute_id = eavTable.attribute_id and eavLabel.store_id = ' . ((int)$storeId), + 'eavLabel.value as label_value' + )->joinInner( + ['weeeTax' => $this->getTable('weee_tax')], + 'weeeTax.attribute_id = eavTable.attribute_id', + 'weeeTax.value as weee_value' + )->where( + 'eavTable.frontend_input = ?', + 'weee' + )->where( + 'weeeTax.website_id IN(?)', + [$websiteId, 0] + )->where( + 'weeeTax.country = ?', + $countryId + )->where( + 'weeeTax.state IN(?)', + [$regionId, 0] + )->where( + 'weeeTax.entity_id = ?', + (int)$entityId + ); - $order = ['weeeTax.state ' . \Magento\Framework\DB\Select::SQL_DESC, - 'weeeTax.website_id ' . \Magento\Framework\DB\Select::SQL_DESC]; - $attributeSelect->order($order); + $order = ['weeeTax.state ' . \Magento\Framework\DB\Select::SQL_DESC, + 'weeeTax.website_id ' . \Magento\Framework\DB\Select::SQL_DESC]; + $attributeSelect->order($order); - $values = $this->getConnection()->fetchAll($attributeSelect); + $values = $this->getConnection()->fetchAll($attributeSelect); - if ($values) { - return $values; + if ($values) { + $this->weeeTaxCalculationsByEntityCache[$cacheKey] = $values; + return $values; + } + } else { + return $this->weeeTaxCalculationsByEntityCache[$cacheKey]; } return []; diff --git a/app/code/Magento/Weee/etc/db_schema.xml b/app/code/Magento/Weee/etc/db_schema.xml index 1b07168247011..aed8318993acf 100644 --- a/app/code/Magento/Weee/etc/db_schema.xml +++ b/app/code/Magento/Weee/etc/db_schema.xml @@ -9,9 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="weee_tax" resource="default" engine="innodb" comment="Weee Tax"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Website Id"/> + default="0" comment="Website ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> <column xsi:type="varchar" name="country" nullable="true" length="2" comment="Country"/> @@ -20,7 +20,7 @@ <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="State"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Attribute Id"/> + comment="Attribute ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php new file mode 100644 index 0000000000000..98164c18e858f --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/FixedProductTax.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Weee\Helper\Data; +use Magento\Framework\Exception\LocalizedException; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Tax\Model\Config; + +/** + * Resolver for FixedProductTax object that retrieves an array of FPT attributes with prices + */ +class FixedProductTax implements ResolverInterface +{ + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $fptArray = []; + $product = $value['model']; + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + if ($this->weeeHelper->isEnabled($store)) { + $attributes = $this->weeeHelper->getProductWeeeAttributesForDisplay($product); + foreach ($attributes as $attribute) { + $displayInclTaxes = $this->taxHelper->getPriceDisplayType($store); + $amount = $attribute->getData('amount'); + //add display mode for WEE to not return WEE if excluded + if ($displayInclTaxes === Config::DISPLAY_TYPE_EXCLUDING_TAX) { + $amount = $attribute->getData('amount_excl_tax'); + } elseif ($displayInclTaxes === Config::DISPLAY_TYPE_INCLUDING_TAX) { + $amount = $attribute->getData('amount_excl_tax') + $attribute->getData('tax_amount'); + } + $fptArray[] = [ + 'amount' => [ + 'value' => $amount, + 'currency' => $value['final_price']['currency'], + ], + 'label' => $attribute->getData('name') + ]; + } + } + + return $fptArray; + } +} diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php new file mode 100644 index 0000000000000..d2ea44fff5bcc --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Weee\Helper\Data; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Framework\Pricing\Render; + +/** + * Resolver for the FPT store config settings + */ +class StoreConfig implements ResolverInterface +{ + /** + * @var string + */ + private static $weeeDisplaySettingsNone = 'FPT_DISABLED'; + + /** + * @var array + */ + private static $weeeDisplaySettings = [ + WeeeDisplayConfig::DISPLAY_INCL => 'INCLUDE_FPT_WITHOUT_DETAILS', + WeeeDisplayConfig::DISPLAY_INCL_DESCR => 'INCLUDE_FPT_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL => 'EXCLUDE_FPT_WITHOUT_DETAILS' + ]; + + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var array + */ + private $computedFptSettings = []; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($this->computedFptSettings)) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + ]; + if ($this->weeeHelper->isEnabled($store)) { + $productFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_VIEW, $storeId); + $categoryFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_LIST, $storeId); + $salesModulesFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_SALES, $storeId); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$productFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$categoryFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$salesModulesFptDisplay] ?? + self::$weeeDisplaySettingsNone, + ]; + } + } + + return $this->computedFptSettings[$info->fieldName] ?? null; + } + + /** + * Get the weee system display setting + * + * @param string $zone + * @param string $storeId + * @return string + */ + private function getWeeDisplaySettingsByZone(string $zone, int $storeId): int + { + return (int) $this->weeeHelper->typeOfDisplay( + null, + $zone, + $storeId + ); + } +} diff --git a/app/code/Magento/WeeeGraphQl/composer.json b/app/code/Magento/WeeeGraphQl/composer.json index 0bf303f789a7f..39b77bb569ac6 100644 --- a/app/code/Magento/WeeeGraphQl/composer.json +++ b/app/code/Magento/WeeeGraphQl/composer.json @@ -4,10 +4,12 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0||~7.3.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-weee": "*" }, "suggest": { - "magento/module-weee": "*", "magento/module-catalog-graph-ql": "*" }, "license": [ diff --git a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls index 731260ce9e1e0..18b0e7c1823e8 100644 --- a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls @@ -2,6 +2,29 @@ # See COPYING.txt for license details. enum PriceAdjustmentCodesEnum { - WEE - WEETAX + WEEE @deprecated(reason: "WEEE code is deprecated, use fixed_product_taxes.label") + WEEE_TAX @deprecated(reason: "Use fixed_product_taxes. PriceAdjustmentCodesEnum is deprecated. Tax is included or excluded in price. Tax is not shown separtely in Catalog") +} + +type ProductPrice { + fixed_product_taxes: [FixedProductTax] @doc(description: "The multiple FPTs that can be applied to a product price.") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\FixedProductTax") +} + +type FixedProductTax @doc(description: "A single FPT that can be applied to a product price.") { + amount: Money @doc(description: "Amount of the FPT as a money object.") + label: String @doc(description: "The label assigned to the FPT to be displayed on the frontend.") +} + +type StoreConfig { + product_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices On Product View Page' field. It indicates how FPT information is displayed on product pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + category_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Product Lists' field. It indicates how FPT information is displayed on category pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + sales_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Sales Modules' field. It indicates how FPT information is displayed on cart, checkout, and order pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") +} + +enum FixedProductTaxDisplaySettings @doc(description: "This enumeration display settings for the fixed product tax") { + INCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price includes the FPT amount without displaying the ProductPrice.fixed_product_taxes values. This value corresponds to 'Including FPT only'") + INCLUDE_FPT_WITH_DETAILS @doc(description: "The displayed price includes the FPT amount while displaying the values of ProductPrice.fixed_product_taxes separately. This value corresponds to 'Including FPT and FPT description'") + EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values of ProductPrice.fixed_product_taxes and the price including the FPT are displayed separately. This value corresponds to 'Excluding FPT, Including FPT description and final price'") + EXCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values from ProductPrice.fixed_product_taxes are not displayed. This value corresponds to 'Excluding FPT'") + FPT_DISABLED @doc(description: "The FPT feature is not enabled. You can omit ProductPrice.fixed_product_taxes from your query") } diff --git a/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml index 2c4e2e70fec71..9ed4e2ced99f7 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/NewProductsListWidgetTest.xml @@ -27,7 +27,7 @@ </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <!-- Create a CMS page containing the New Products widget --> diff --git a/app/code/Magento/Widget/etc/db_schema.xml b/app/code/Magento/Widget/etc/db_schema.xml index a82e6aae20296..6146761f6f251 100644 --- a/app/code/Magento/Widget/etc/db_schema.xml +++ b/app/code/Magento/Widget/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="widget" resource="default" engine="innodb" comment="Preconfigured Widgets"> <column xsi:type="int" name="widget_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Widget Id"/> + comment="Widget ID"/> <column xsi:type="varchar" name="widget_code" nullable="true" length="255" comment="Widget code for template directive"/> <column xsi:type="varchar" name="widget_type" nullable="true" length="255" comment="Widget Type"/> @@ -23,10 +23,10 @@ </table> <table name="widget_instance" resource="default" engine="innodb" comment="Instances of Widget for Package Theme"> <column xsi:type="int" name="instance_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Instance Id"/> + comment="Instance ID"/> <column xsi:type="varchar" name="instance_type" nullable="true" length="255" comment="Instance Type"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme id"/> + comment="Theme ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Widget Title"/> <column xsi:type="varchar" name="store_ids" nullable="false" length="255" default="0" comment="Store ids"/> <column xsi:type="text" name="widget_parameters" nullable="true" comment="Widget parameters"/> @@ -40,9 +40,9 @@ </table> <table name="widget_instance_page" resource="default" engine="innodb" comment="Instance of Widget on Page"> <column xsi:type="int" name="page_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Page Id"/> + comment="Page ID"/> <column xsi:type="int" name="instance_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Instance Id"/> + default="0" comment="Instance ID"/> <column xsi:type="varchar" name="page_group" nullable="true" length="25" comment="Block Group Type"/> <column xsi:type="varchar" name="layout_handle" nullable="true" length="255" comment="Layout Handle"/> <column xsi:type="varchar" name="block_reference" nullable="true" length="255" comment="Container"/> @@ -61,9 +61,9 @@ </table> <table name="widget_instance_page_layout" resource="default" engine="innodb" comment="Layout updates"> <column xsi:type="int" name="page_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Page Id"/> + comment="Page ID"/> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Layout Update Id"/> + default="0" comment="Layout Update ID"/> <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID_WIDGET_INSTANCE_PAGE_PAGE_ID" table="widget_instance_page_layout" column="page_id" referenceTable="widget_instance_page" referenceColumn="page_id" onDelete="CASCADE"/> @@ -80,7 +80,7 @@ </table> <table name="layout_update" resource="default" engine="innodb" comment="Layout Updates"> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Layout Update Id"/> + comment="Layout Update ID"/> <column xsi:type="varchar" name="handle" nullable="true" length="255" comment="Handle"/> <column xsi:type="text" name="xml" nullable="true" comment="Xml"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" @@ -96,13 +96,13 @@ </table> <table name="layout_link" resource="default" engine="innodb" comment="Layout Link"> <column xsi:type="int" name="layout_link_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Link Id"/> + comment="Link ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="int" name="theme_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Theme id"/> + comment="Theme ID"/> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Layout Update Id"/> + default="0" comment="Layout Update ID"/> <column xsi:type="boolean" name="is_temporary" nullable="false" default="false" comment="Defines whether Layout Update is Temporary"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php index fb6b647811abb..6ef55bbe81b73 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection/Grid.php @@ -6,10 +6,12 @@ namespace Magento\Wishlist\Model\ResourceModel\Item\Collection; -use Magento\Customer\Controller\RegistryConstants as RegistryConstants; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Wishlist\Model\Item; /** - * Wishlist item collection grouped by customer id + * Wishlist item collection for grid grouped by customer id * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -88,30 +90,48 @@ public function __construct( } /** - * Initialize db select - * - * @return $this + * @inheritdoc */ protected function _initSelect() { parent::_initSelect(); - $this->addCustomerIdFilter( - $this->_registryManager->registry(RegistryConstants::CURRENT_CUSTOMER_ID) - ) - ->resetSortOrder() - ->addDaysInWishlist() + + $customerId = $this->_registryManager->registry(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->addDaysInWishlist() ->addStoreData() - ->setVisibilityFilter() - ->setInStockFilter(); + ->addCustomerIdFilter($customerId) + ->resetSortOrder(); + return $this; } /** - * Add select order - * - * @param string $field - * @param string $direction - * @return \Magento\Framework\Data\Collection\AbstractDb + * @inheritdoc + */ + protected function _assignProducts() + { + /** @var ProductCollection $productCollection */ + $productCollection = $this->_productCollectionFactory->create() + ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()) + ->addIdFilter($this->_productIds); + + /** @var Item $item */ + foreach ($this as $item) { + $product = $productCollection->getItemById($item->getProductId()); + if ($product) { + $product->setCustomOptions([]); + $item->setProduct($product); + $item->setProductName($product->getName()); + $item->setName($product->getName()); + $item->setPrice($product->getPrice()); + } + } + + return $this; + } + + /** + * @inheritdoc */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) { @@ -127,24 +147,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } /** - * Add quantity to filter - * - * @param string $field - * @param array $condition - * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection - */ - private function addQtyFilter(string $field, array $condition) - { - return parent::addFieldToFilter('main_table.' . $field, $condition); - } - - /** - * Add field filter to collection - * - * @param string|array $field - * @param null|string|array $condition - * @see self::_getConditionSql for $condition - * @return \Magento\Framework\Data\Collection\AbstractDb + * @inheritdoc */ public function addFieldToFilter($field, $condition = null) { @@ -168,6 +171,19 @@ public function addFieldToFilter($field, $condition = null) return $this->addQtyFilter($field, $condition); } } + return parent::addFieldToFilter($field, $condition); } + + /** + * Add quantity to filter + * + * @param string $field + * @param array $condition + * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + private function addQtyFilter(string $field, array $condition) + { + return parent::addFieldToFilter('main_table.' . $field, $condition); + } } diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 9797ab58b0766..9b7ff5177afae 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -7,10 +7,30 @@ namespace Magento\Wishlist\Model; +use Exception; +use InvalidArgumentException; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Math\Random; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Wishlist\Helper\Data; use Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory; use Magento\Wishlist\Model\ResourceModel\Wishlist as ResourceWishlist; use Magento\Wishlist\Model\ResourceModel\Wishlist\Collection; @@ -19,21 +39,21 @@ * Wishlist model * * @method int getShared() - * @method \Magento\Wishlist\Model\Wishlist setShared(int $value) + * @method Wishlist setShared(int $value) * @method string getSharingCode() - * @method \Magento\Wishlist\Model\Wishlist setSharingCode(string $value) + * @method Wishlist setSharingCode(string $value) * @method string getUpdatedAt() - * @method \Magento\Wishlist\Model\Wishlist setUpdatedAt(string $value) + * @method Wishlist setUpdatedAt(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * * @api * @since 100.0.2 */ -class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magento\Framework\DataObject\IdentityInterface +class Wishlist extends AbstractModel implements IdentityInterface { /** - * Cache tag + * Wishlist cache tag name */ const CACHE_TAG = 'wishlist'; @@ -47,14 +67,14 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent /** * Wishlist item collection * - * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @var ResourceModel\Item\Collection */ protected $_itemCollection; /** * Store filter for wishlist * - * @var \Magento\Store\Model\Store + * @var Store */ protected $_store; @@ -68,7 +88,7 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent /** * Wishlist data * - * @var \Magento\Wishlist\Helper\Data + * @var Data */ protected $_wishlistData; @@ -80,12 +100,12 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent protected $_catalogProduct; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var DateTime\DateTime */ protected $_date; @@ -100,17 +120,17 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent protected $_wishlistCollectionFactory; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $_productFactory; /** - * @var \Magento\Framework\Math\Random + * @var Random */ protected $mathRandom; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $dateTime; @@ -129,46 +149,60 @@ class Wishlist extends \Magento\Framework\Model\AbstractModel implements \Magent */ private $serializer; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StockRegistryInterface|null + */ + private $stockRegistry; + /** * Constructor * - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry + * @param Context $context + * @param Registry $registry * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param \Magento\Wishlist\Helper\Data $wishlistData + * @param Data $wishlistData * @param ResourceWishlist $resource * @param Collection $resourceCollection - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Stdlib\DateTime\DateTime $date + * @param StoreManagerInterface $storeManager + * @param DateTime\DateTime $date * @param ItemFactory $wishlistItemFactory * @param CollectionFactory $wishlistCollectionFactory - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Framework\Math\Random $mathRandom - * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param ProductFactory $productFactory + * @param Random $mathRandom + * @param DateTime $dateTime * @param ProductRepositoryInterface $productRepository * @param bool $useCurrentWebsite * @param array $data * @param Json|null $serializer + * @param StockRegistryInterface|null $stockRegistry + * @param ScopeConfigInterface|null $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, + Context $context, + Registry $registry, \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\Wishlist\Helper\Data $wishlistData, + Data $wishlistData, ResourceWishlist $resource, Collection $resourceCollection, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Stdlib\DateTime\DateTime $date, + StoreManagerInterface $storeManager, + DateTime\DateTime $date, ItemFactory $wishlistItemFactory, CollectionFactory $wishlistCollectionFactory, - \Magento\Catalog\Model\ProductFactory $productFactory, - \Magento\Framework\Math\Random $mathRandom, - \Magento\Framework\Stdlib\DateTime $dateTime, + ProductFactory $productFactory, + Random $mathRandom, + DateTime $dateTime, ProductRepositoryInterface $productRepository, $useCurrentWebsite = true, array $data = [], - Json $serializer = null + Json $serializer = null, + StockRegistryInterface $stockRegistry = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_useCurrentWebsite = $useCurrentWebsite; $this->_catalogProduct = $catalogProduct; @@ -183,6 +217,8 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->productRepository = $productRepository; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->stockRegistry = $stockRegistry ?: ObjectManager::getInstance()->get(StockRegistryInterface::class); } /** @@ -290,13 +326,13 @@ public function afterSave() /** * Add catalog product object data to wishlist * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param int $qty * @param bool $forciblySetQty * * @return Item */ - protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $qty = 1, $forciblySetQty = false) + protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQty = false) { $item = null; foreach ($this->getItemCollection() as $_item) { @@ -311,7 +347,7 @@ protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $ $item = $this->_wishlistItemFactory->create(); $item->setProductId($product->getId()); $item->setWishlistId($this->getId()); - $item->setAddedAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); + $item->setAddedAt((new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT)); $item->setStoreId($storeId); $item->setOptions($product->getCustomOptions()); $item->setProduct($product); @@ -334,6 +370,7 @@ protected function _addCatalogProduct(\Magento\Catalog\Model\Product $product, $ * Retrieve wishlist item collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @throws NoSuchEntityException */ public function getItemCollection() { @@ -365,8 +402,9 @@ public function getItem($itemId) /** * Adding item to wishlist * - * @param Item $item - * @return $this + * @param Item $item + * @return $this + * @throws Exception */ public function addItem(Item $item) { @@ -383,13 +421,14 @@ public function addItem(Item $item) * * Returns new item or string on error. * - * @param int|\Magento\Catalog\Model\Product $product - * @param \Magento\Framework\DataObject|array|string|null $buyRequest + * @param int|Product $product + * @param DataObject|array|string|null $buyRequest * @param bool $forciblySetQty - * @throws \Magento\Framework\Exception\LocalizedException * @return Item|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws LocalizedException + * @throws InvalidArgumentException */ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false) { @@ -398,7 +437,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * a) we have new instance and do not interfere with other products in wishlist * b) product has full set of attributes */ - if ($product instanceof \Magento\Catalog\Model\Product) { + if ($product instanceof Product) { $productId = $product->getId(); // Maybe force some store by wishlist internal properties $storeId = $product->hasWishlistStoreId() ? $product->getWishlistStoreId() : $product->getStoreId(); @@ -412,12 +451,17 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false } try { + /** @var Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('Cannot specify product.')); + throw new LocalizedException(__('Cannot specify product.')); + } + + if ($this->isInStock($productId)) { + throw new LocalizedException(__('Cannot add product without stock to wishlist.')); } - if ($buyRequest instanceof \Magento\Framework\DataObject) { + if ($buyRequest instanceof DataObject) { $_buyRequest = $buyRequest; } elseif (is_string($buyRequest)) { $isInvalidItemConfiguration = false; @@ -426,20 +470,20 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false if (!is_array($buyRequestData)) { $isInvalidItemConfiguration = true; } - } catch (\InvalidArgumentException $exception) { + } catch (Exception $exception) { $isInvalidItemConfiguration = true; } if ($isInvalidItemConfiguration) { - throw new \InvalidArgumentException('Invalid wishlist item configuration.'); + throw new InvalidArgumentException('Invalid wishlist item configuration.'); } - $_buyRequest = new \Magento\Framework\DataObject($buyRequestData); + $_buyRequest = new DataObject($buyRequestData); } elseif (is_array($buyRequest)) { - $_buyRequest = new \Magento\Framework\DataObject($buyRequest); + $_buyRequest = new DataObject($buyRequest); } else { - $_buyRequest = new \Magento\Framework\DataObject(); + $_buyRequest = new DataObject(); } - /* @var $product \Magento\Catalog\Model\Product */ + /* @var $product Product */ $cartCandidates = $product->getTypeInstance()->processConfiguration($_buyRequest, clone $product); /** @@ -486,6 +530,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * * @param int $customerId * @return $this + * @throws LocalizedException */ public function setCustomerId($customerId) { @@ -496,6 +541,7 @@ public function setCustomerId($customerId) * Retrieve customer id * * @return int + * @throws LocalizedException */ public function getCustomerId() { @@ -506,6 +552,7 @@ public function getCustomerId() * Retrieve data for save * * @return array + * @throws LocalizedException */ public function getDataForSave() { @@ -520,6 +567,7 @@ public function getDataForSave() * Retrieve shared store ids for current website or all stores if $current is false * * @return array + * @throws NoSuchEntityException */ public function getSharedStoreIds() { @@ -554,6 +602,7 @@ public function setSharedStoreIds($storeIds) * Retrieve wishlist store object * * @return \Magento\Store\Model\Store + * @throws NoSuchEntityException */ public function getStore() { @@ -566,7 +615,7 @@ public function getStore() /** * Set wishlist store * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return $this */ public function setStore($store) @@ -600,11 +649,30 @@ public function isSalable() return false; } + /** + * Retrieve if product has stock or config is set for showing out of stock products + * + * @param int $productId + * @return bool + */ + private function isInStock($productId) + { + /** @var StockItemInterface $stockItem */ + $stockItem = $this->stockRegistry->getStockItem($productId); + $showOutOfStock = $this->scopeConfig->isSetFlag( + Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + ScopeInterface::SCOPE_STORE + ); + $isInStock = $stockItem ? $stockItem->getIsInStock() : false; + return !$isInStock && !$showOutOfStock; + } + /** * Check customer is owner this wishlist * * @param int $customerId * @return bool + * @throws LocalizedException */ public function isOwner($customerId) { @@ -626,10 +694,10 @@ public function isOwner($customerId) * For more options see \Magento\Catalog\Helper\Product->addParamsToBuyRequest() * * @param int|Item $itemId - * @param \Magento\Framework\DataObject $buyRequest - * @param null|array|\Magento\Framework\DataObject $params + * @param DataObject $buyRequest + * @param null|array|DataObject $params * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * * @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest() * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -645,16 +713,16 @@ public function updateItem($itemId, $buyRequest, $params = null) $item = $this->getItem((int)$itemId); } if (!$item) { - throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t specify a wish list item.')); + throw new LocalizedException(__('We can\'t specify a wish list item.')); } $product = $item->getProduct(); $productId = $product->getId(); if ($productId) { if (!$params) { - $params = new \Magento\Framework\DataObject(); + $params = new DataObject(); } elseif (is_array($params)) { - $params = new \Magento\Framework\DataObject($params); + $params = new DataObject($params); } $params->setCurrentConfig($item->getBuyRequest()); $buyRequest = $this->_catalogProduct->addParamsToBuyRequest($buyRequest, $params); @@ -677,7 +745,7 @@ public function updateItem($itemId, $buyRequest, $params = null) * Error message */ if (is_string($resultItem)) { - throw new \Magento\Framework\Exception\LocalizedException(__($resultItem)); + throw new LocalizedException(__($resultItem)); } if ($resultItem->getId() != $itemId) { @@ -691,7 +759,7 @@ public function updateItem($itemId, $buyRequest, $params = null) $resultItem->setOrigData('qty', 0); } } else { - throw new \Magento\Framework\Exception\LocalizedException(__('The product does not exist.')); + throw new LocalizedException(__('The product does not exist.')); } return $this; } diff --git a/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php b/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php deleted file mode 100644 index 5c65fce10ccd2..0000000000000 --- a/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Wishlist\Setup\Patch\Schema; - -use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\Setup\Patch\SchemaPatchInterface; -use Magento\Framework\Setup\SchemaSetupInterface; - -/** - * Class AddProductIdConstraint - */ -class AddProductIdConstraint implements SchemaPatchInterface -{ - /** - * @var SchemaSetupInterface - */ - private $schemaSetup; - - /** - * @param SchemaSetupInterface $schemaSetup - */ - public function __construct( - SchemaSetupInterface $schemaSetup - ) { - $this->schemaSetup = $schemaSetup; - } - - /** - * Run code inside patch. - * - * @return void - */ - public function apply() - { - $this->schemaSetup->startSetup(); - - $this->schemaSetup->getConnection()->addForeignKey( - $this->schemaSetup->getConnection()->getForeignKeyName( - $this->schemaSetup->getTable('wishlist_item_option'), - 'product_id', - $this->schemaSetup->getTable('catalog_product_entity'), - 'entity_id' - ), - $this->schemaSetup->getTable('wishlist_item_option'), - 'product_id', - $this->schemaSetup->getTable('catalog_product_entity'), - 'entity_id', - AdapterInterface::FK_ACTION_CASCADE, - true - ); - - $this->schemaSetup->endSetup(); - } - - /** - * Get array of patches that have to be executed prior to this. - * - * @return string[] - */ - public static function getDependencies() - { - return []; - } - - /** - * Get aliases (previous names) for the patch. - * - * @return string[] - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index 6b951c89208c2..0489ec750b7e0 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -32,7 +32,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index e8b645990390e..aeb1d134d8e22 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -48,7 +48,7 @@ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsFilters"/> <!--Logout everywhere--> - <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <actionGroup ref="logout" stepKey="adminLogout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> </after> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml index 3c84562542adf..f1659baaa4e09 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml @@ -32,7 +32,7 @@ <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct" createDataKey="product"/> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="adminLogout"/> </after> <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml index e3382dc41d27e..6c73cb6708ae4 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml @@ -34,6 +34,11 @@ <deleteData createDataKey="categorySecond" stepKey="deleteCategorySecond"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> + + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index e482449f623fc..b8a84a327b58f 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -26,6 +26,10 @@ <createData entity="Simple_US_Customer" stepKey="customer"/> </before> + <!-- Perform reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> </actionGroup> diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php index ff8a3a3b87cec..eb788efc0d622 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php @@ -5,76 +5,104 @@ */ namespace Magento\Wishlist\Test\Unit\Model; +use ArrayIterator; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Catalog\Model\ProductFactory; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Stock\Item as StockItem; +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Math\Random; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Wishlist\Helper\Data; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\ItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\ResourceModel\Wishlist\Collection as WishlistCollection; use Magento\Wishlist\Model\Wishlist; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class WishlistTest extends \PHPUnit\Framework\TestCase +class WishlistTest extends TestCase { /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + * @var Registry|PHPUnit_Framework_MockObject_MockObject */ protected $registry; /** - * @var \Magento\Catalog\Helper\Product|\PHPUnit_Framework_MockObject_MockObject + * @var HelperProduct|PHPUnit_Framework_MockObject_MockObject */ protected $productHelper; /** - * @var \Magento\Wishlist\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|PHPUnit_Framework_MockObject_MockObject */ protected $helper; /** - * @var \Magento\Wishlist\Model\ResourceModel\Wishlist|\PHPUnit_Framework_MockObject_MockObject + * @var WishlistResource|PHPUnit_Framework_MockObject_MockObject */ protected $resource; /** - * @var \Magento\Wishlist\Model\ResourceModel\Wishlist\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var WishlistCollection|PHPUnit_Framework_MockObject_MockObject */ protected $collection; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|PHPUnit_Framework_MockObject_MockObject */ protected $storeManager; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var DateTime\DateTime|PHPUnit_Framework_MockObject_MockObject */ protected $date; /** - * @var \Magento\Wishlist\Model\ItemFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ItemFactory|PHPUnit_Framework_MockObject_MockObject */ protected $itemFactory; /** - * @var \Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject */ protected $itemsFactory; /** - * @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ProductFactory|PHPUnit_Framework_MockObject_MockObject */ protected $productFactory; /** - * @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject + * @var Random|PHPUnit_Framework_MockObject_MockObject */ protected $mathRandom; /** - * @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var DateTime|PHPUnit_Framework_MockObject_MockObject */ protected $dateTime; /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|PHPUnit_Framework_MockObject_MockObject */ protected $eventDispatcher; @@ -84,63 +112,79 @@ class WishlistTest extends \PHPUnit\Framework\TestCase protected $wishlist; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ProductRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ protected $productRepository; /** - * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|PHPUnit_Framework_MockObject_MockObject */ protected $serializer; + /** + * @var StockItemRepository|PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var StockRegistryInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $stockRegistry; + protected function setUp() { - $context = $this->getMockBuilder(\Magento\Framework\Model\Context::class) + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); - $this->eventDispatcher = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + $this->eventDispatcher = $this->getMockBuilder(ManagerInterface::class) ->getMock(); - $this->registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + $this->registry = $this->getMockBuilder(Registry::class) ->disableOriginalConstructor() ->getMock(); - $this->productHelper = $this->getMockBuilder(\Magento\Catalog\Helper\Product::class) + $this->productHelper = $this->getMockBuilder(HelperProduct::class) ->disableOriginalConstructor() ->getMock(); - $this->helper = $this->getMockBuilder(\Magento\Wishlist\Helper\Data::class) + $this->helper = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); - $this->resource = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Wishlist::class) + $this->resource = $this->getMockBuilder(WishlistResource::class) ->disableOriginalConstructor() ->getMock(); - $this->collection = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Wishlist\Collection::class) + $this->collection = $this->getMockBuilder(WishlistCollection::class) ->disableOriginalConstructor() ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) ->getMock(); - $this->date = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) + $this->date = $this->getMockBuilder(DateTime\DateTime::class) ->disableOriginalConstructor() ->getMock(); - $this->itemFactory = $this->getMockBuilder(\Magento\Wishlist\Model\ItemFactory::class) + $this->itemFactory = $this->getMockBuilder(ItemFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->itemsFactory = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory::class) + $this->itemsFactory = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productFactory = $this->getMockBuilder(\Magento\Catalog\Model\ProductFactory::class) + $this->productFactory = $this->getMockBuilder(ProductFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->mathRandom = $this->getMockBuilder(\Magento\Framework\Math\Random::class) + $this->mathRandom = $this->getMockBuilder(Random::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTime = $this->getMockBuilder(DateTime::class) ->disableOriginalConstructor() ->getMock(); - $this->dateTime = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime::class) + $this->productRepository = $this->createMock(ProductRepositoryInterface::class); + $this->stockRegistry = $this->createMock(StockRegistryInterface::class); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->productRepository = $this->createMock(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->serializer = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class) + $this->serializer = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); @@ -165,7 +209,9 @@ protected function setUp() $this->productRepository, false, [], - $this->serializer + $this->serializer, + $this->stockRegistry, + $this->scopeConfig ); } @@ -186,7 +232,7 @@ public function testLoadByCustomerId() ->will($this->returnValue($sharingCode)); $this->assertInstanceOf( - \Magento\Wishlist\Model\Wishlist::class, + Wishlist::class, $this->wishlist->loadByCustomerId($customerId, true) ); $this->assertEquals($customerId, $this->wishlist->getCustomerId()); @@ -194,10 +240,10 @@ public function testLoadByCustomerId() } /** - * @param int|\Magento\Wishlist\Model\Item|\PHPUnit_Framework_MockObject_MockObject $itemId - * @param \Magento\Framework\DataObject $buyRequest - * @param null|array|\Magento\Framework\DataObject $param - * @throws \Magento\Framework\Exception\LocalizedException + * @param int|Item|PHPUnit_Framework_MockObject_MockObject $itemId + * @param DataObject $buyRequest + * @param null|array|DataObject $param + * @throws LocalizedException * * @dataProvider updateItemDataProvider */ @@ -205,9 +251,9 @@ public function testUpdateItem($itemId, $buyRequest, $param) { $storeId = 1; $productId = 1; - $stores = [(new \Magento\Framework\DataObject())->setId($storeId)]; + $stores = [(new DataObject())->setId($storeId)]; - $newItem = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $newItem = $this->getMockBuilder(Item::class) ->setMethods( ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] ) @@ -228,26 +274,30 @@ public function testUpdateItem($itemId, $buyRequest, $param) $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($stores[0])); $product = $this->getMockBuilder( - \Magento\Catalog\Model\Product::class + Product::class )->disableOriginalConstructor()->getMock(); $product->expects($this->any())->method('getId')->will($this->returnValue($productId)); $product->expects($this->any())->method('getStoreId')->will($this->returnValue($storeId)); - $instanceType = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + $stockItem = $this->getMockBuilder(StockItem::class)->disableOriginalConstructor()->getMock(); + $stockItem->expects($this->any())->method('getIsInStock')->will($this->returnValue(true)); + $this->stockRegistry->expects($this->any()) + ->method('getStockItem') + ->will($this->returnValue($stockItem)); + + $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); $instanceType->expects($this->once()) ->method('processConfiguration') ->will( $this->returnValue( - $this->getMockBuilder( - \Magento\Catalog\Model\Product::class - )->disableOriginalConstructor()->getMock() + $this->getMockBuilder(Product::class)->disableOriginalConstructor()->getMock() ) ); $newProduct = $this->getMockBuilder( - \Magento\Catalog\Model\Product::class + Product::class )->disableOriginalConstructor()->getMock(); $newProduct->expects($this->any()) ->method('setStoreId') @@ -257,12 +307,12 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->method('getTypeInstance') ->will($this->returnValue($instanceType)); - $item = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class)->disableOriginalConstructor()->getMock(); + $item = $this->getMockBuilder(Item::class)->disableOriginalConstructor()->getMock(); $item->expects($this->once()) ->method('getProduct') ->will($this->returnValue($product)); - $items = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Collection::class) + $items = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -280,7 +330,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->will($this->returnValue($item)); $items->expects($this->any()) ->method('getIterator') - ->will($this->returnValue(new \ArrayIterator([$item]))); + ->will($this->returnValue(new ArrayIterator([$item]))); $this->itemsFactory->expects($this->any()) ->method('create') @@ -292,7 +342,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) ->will($this->returnValue($newProduct)); $this->assertInstanceOf( - \Magento\Wishlist\Model\Wishlist::class, + Wishlist::class, $this->wishlist->updateItem($itemId, $buyRequest, $param) ); } @@ -303,7 +353,7 @@ public function testUpdateItem($itemId, $buyRequest, $param) public function updateItemDataProvider() { return [ - '0' => [1, new \Magento\Framework\DataObject(), null] + '0' => [1, new DataObject(), null] ]; } @@ -311,24 +361,26 @@ public function testAddNewItem() { $productId = 1; $storeId = 1; - $buyRequest = json_encode([ - 'number' => 42, - 'string' => 'string_value', - 'boolean' => true, - 'collection' => [1, 2, 3], - 'product' => 1, - 'form_key' => 'abc' - ]); + $buyRequest = json_encode( + [ + 'number' => 42, + 'string' => 'string_value', + 'boolean' => true, + 'collection' => [1, 2, 3], + 'product' => 1, + 'form_key' => 'abc' + ] + ); $result = 'product'; - $instanceType = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + $instanceType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMock(); $instanceType->expects($this->once()) ->method('processConfiguration') ->willReturn('product'); - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getId', 'hasWishlistStoreId', 'getStoreId', 'getTypeInstance']) ->getMock(); @@ -358,6 +410,15 @@ function ($value) { } ); + $stockItem = $this->getMockBuilder( + StockItem::class + )->disableOriginalConstructor()->getMock(); + $stockItem->expects($this->any())->method('getIsInStock')->will($this->returnValue(true)); + + $this->stockRegistry->expects($this->any()) + ->method('getStockItem') + ->will($this->returnValue($stockItem)); + $this->assertEquals($result, $this->wishlist->addNewItem($productMock, $buyRequest)); } } diff --git a/app/code/Magento/Wishlist/etc/db_schema.xml b/app/code/Magento/Wishlist/etc/db_schema.xml index 8a02f411ad0db..e430a1ee40ea2 100644 --- a/app/code/Magento/Wishlist/etc/db_schema.xml +++ b/app/code/Magento/Wishlist/etc/db_schema.xml @@ -64,11 +64,11 @@ </table> <table name="wishlist_item_option" resource="default" engine="innodb" comment="Wishlist Item Option Table"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Option Id"/> + comment="Option ID"/> <column xsi:type="int" name="wishlist_item_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Wishlist Item Id"/> + comment="Wishlist Item ID"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Product Id"/> + comment="Product ID"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> @@ -77,5 +77,9 @@ <constraint xsi:type="foreign" referenceId="FK_A014B30B04B72DD0EAB3EECD779728D6" table="wishlist_item_option" column="wishlist_item_id" referenceTable="wishlist_item" referenceColumn="wishlist_item_id" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="WISHLIST_ITEM_OPTION_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + table="wishlist_item_option" + column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" + onDelete="CASCADE"/> </table> </schema> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 033e2e43a3c22..aca843872af65 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -79,7 +79,9 @@ define([ $(element).is('textarea') || $('#' + element.id + ' option:selected').length ) { - dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); + if ($(element).data('selector') || $(element).attr('name')) { + dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); + } return; } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less index 40ebb6f3c4569..6b30bf70772a4 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less @@ -97,13 +97,14 @@ display: inline-block; font-size: @notifications__font-size; font-weight: @font-weight__bold; - height: 18px; + height: 20px; left: 50%; + line-height: 20px; margin-left: .3em; margin-top: -1.1em; - min-width: 18px; - padding: .3em .5em; + min-width: 20px; position: absolute; + text-align: center; top: 50%; } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less index d9e2cfdd66bf7..dd67220db12da 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Banner/web/css/source/_module.less @@ -24,6 +24,7 @@ input[type='checkbox'].banner-content-checkbox { } .adminhtml-widget_instance-edit, +.adminhtml-cms_page-edit, .adminhtml-banner-edit { .admin__fieldset { .admin__field-control { diff --git a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less index 659b1fa811db1..fa158589feb96 100644 --- a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less +++ b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less @@ -22,10 +22,10 @@ position: relative; display: -webkit-inline-flex; display: -ms-inline-flexbox; + display: inline-flex; -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; - display: inline-flex; flex-flow: row nowrap; width: 100%; diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less index ffa5ee963952c..a33c1fac083fa 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less @@ -308,4 +308,23 @@ } } +// +// Create Order - Add Product Grid +// --------------------------------------------- + +#sales_order_create_search_grid { + .col-in_products { + .data-grid-checkbox-cell-inner { + position: relative; + } + .checkbox { + width: 1.6rem; + height: 1.6rem; + left: 0; + right: 0; + margin: auto; + } + } +} + // ToDo: MAGETWO-32299 UI: review the collapsible block diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less index c6f39e8e8840d..ab4bac919ee5f 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less @@ -59,15 +59,15 @@ .admin__control-select { &:extend(.abs-form-control-pattern all); .lib-css(appearance, none, 1); - background-image+: url('../images/arrows-bg.svg'); + background-image+: url('../images/arrows-bg.svg') !important; background-position+: ~'calc(100% - 12px)' -34px; background-size+: auto; - background-image+: linear-gradient(@color-gray89, @color-gray89); + background-image+: linear-gradient(@color-gray89, @color-gray89) !important; background-position+: 100%; background-size+: @field-control__height 100%; - background-image+: linear-gradient(@field-control__border-color,@field-control__border-color); + background-image+: linear-gradient(@field-control__border-color,@field-control__border-color) !important; background-position+: ~'calc(100% - @{field-control__height})' 0; background-size+: 1px 100%; @@ -86,13 +86,13 @@ } &:active { - background-image+: url('../images/arrows-bg.svg'); + background-image+: url('../images/arrows-bg.svg') !important; background-position+: ~'calc(100% - 12px)' 13px; - background-image+: linear-gradient(@color-gray89, @color-gray89); + background-image+: linear-gradient(@color-gray89, @color-gray89) !important; background-position+: 100%; - background-image+: linear-gradient(@field-control__focus__border-color, @field-control__focus__border-color); + background-image+: linear-gradient(@field-control__focus__border-color, @field-control__focus__border-color) !important; background-position+: ~'calc(100% - @{field-control__height})' 0; border-color: @field-control__focus__border-color; } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 08aeb35d7adb2..ddc6aa42c23eb 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -122,7 +122,7 @@ > .admin__field-control { #mix-grid .column(@field-control-grid__column, @field-grid__columns); input[type="checkbox"] { - margin-top: @indent__s; + margin-top: 0; } } @@ -156,7 +156,7 @@ .admin__field { margin-top: 8px; } - } + } } } &.composite-bundle { @@ -307,7 +307,7 @@ .admin__fieldset > & { margin-bottom: 3rem; position: relative; - + &.field-import_file { .input-file { margin-top: 6px; @@ -361,6 +361,11 @@ cursor: inherit; opacity: 1; outline: inherit; + .admin__action-multiselect-wrap { + .admin__action-multiselect { + .__form-control-pattern__disabled(); + } + } } &._hidden { @@ -664,7 +669,7 @@ display: inline-block; } } - + + .admin__field:last-child { width: auto; @@ -700,7 +705,7 @@ width: 100%; } } - & > .admin__field-label { + & > .admin__field-label { text-align: left; } diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 44fca79c31be5..b2afde435a627 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -4070,6 +4070,21 @@ } } +.adminhtml-email_template-preview { + .cms-revision-preview { + padding-top: 56.25%; + position: relative; + + #preview_iframe { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } +} + .admin__scope-old { .buttons-set { margin: 0 0 15px; diff --git a/app/etc/di.xml b/app/etc/di.xml index 882d1be623988..335743aef8eed 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1781,4 +1781,5 @@ <type name="Magento\Framework\DB\Adapter\AdapterInterface"> <plugin name="execute_commit_callbacks" type="Magento\Framework\Model\ExecuteCommitCallbacks" /> </type> + <preference for="Magento\Framework\GraphQl\Query\ErrorHandlerInterface" type="Magento\Framework\GraphQl\Query\ErrorHandler"/> </config> diff --git a/app/etc/graphql/di.xml b/app/etc/graphql/di.xml deleted file mode 100644 index aba60d00080ff..0000000000000 --- a/app/etc/graphql/di.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Framework\GraphQl\Query\ErrorHandlerInterface" type="Magento\Framework\GraphQl\Query\ErrorHandler"/> -</config> diff --git a/app/functions.php b/app/functions.php index 4b00d01819f70..6b3dae71c42c6 100644 --- a/app/functions.php +++ b/app/functions.php @@ -13,6 +13,9 @@ * @return \Magento\Framework\Phrase */ if (!function_exists('__')) { + /** + * @return \Magento\Framework\Phrase + */ function __() { $argc = func_get_args(); diff --git a/composer.json b/composer.json index 4a179f480c9b0..ba7f28678ffdd 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,6 @@ "ext-pdo_mysql": "*", "ext-simplexml": "*", "ext-soap": "*", - "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", "lib-libxml": "*", @@ -43,7 +42,7 @@ "wikimedia/less.php": "~1.8.0", "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^2.0.0", - "php-amqplib/php-amqplib": "~2.7.0", + "php-amqplib/php-amqplib": "~2.7.0|~2.10.0", "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", "ramsey/uuid": "~3.8.0", @@ -88,7 +87,7 @@ "friendsofphp/php-cs-fixer": "~2.14.0", "lusitanian/oauth": "~0.8.10", "magento/magento-coding-standard": "~4.0.0", - "magento/magento2-functional-testing-framework": "2.4.3", + "magento/magento2-functional-testing-framework": "2.5.0", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", @@ -123,6 +122,7 @@ "magento/module-captcha": "*", "magento/module-cardinal-commerce": "*", "magento/module-catalog": "*", + "magento/module-catalog-customer-graph-ql": "*", "magento/module-catalog-analytics": "*", "magento/module-catalog-import-export": "*", "magento/module-catalog-inventory": "*", diff --git a/composer.lock b/composer.lock index cb4f029182219..3d9f7d19530fc 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "fe4a8dce06cfede9180e774e43149550", + "content-hash": "7ff21794a9a2266584e59855a064c992", "packages": [ { "name": "braintree/braintree_php", @@ -201,16 +201,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.1", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "33810d865dd06a674130fceb729b2f279dc79e8c" + "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/33810d865dd06a674130fceb729b2f279dc79e8c", - "reference": "33810d865dd06a674130fceb729b2f279dc79e8c", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527", + "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527", "shasum": "" }, "require": { @@ -253,20 +253,20 @@ "ssl", "tls" ], - "time": "2019-07-31T08:13:16+00:00" + "time": "2019-08-30T08:44:50+00:00" }, { "name": "composer/composer", - "version": "1.8.6", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "19b5f66a0e233eb944f134df34091fe1c5dfcc11" + "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/19b5f66a0e233eb944f134df34091fe1c5dfcc11", - "reference": "19b5f66a0e233eb944f134df34091fe1c5dfcc11", + "url": "https://api.github.com/repos/composer/composer/zipball/314aa57fdcfc942065996f59fb73a8b3f74f3fa5", + "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5", "shasum": "" }, "require": { @@ -302,7 +302,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -326,14 +326,14 @@ "homepage": "http://seld.be" } ], - "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", "homepage": "https://getcomposer.org/", "keywords": [ "autoload", "dependency", "package" ], - "time": "2019-06-11T13:03:06+00:00" + "time": "2019-08-02T18:55:33+00:00" }, { "name": "composer/semver", @@ -1110,16 +1110,16 @@ }, { "name": "monolog/monolog", - "version": "1.24.0", + "version": "1.25.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", "shasum": "" }, "require": { @@ -1184,7 +1184,7 @@ "logging", "psr-3" ], - "time": "2018-11-05T09:00:11+00:00" + "time": "2019-09-06T13:49:17+00:00" }, { "name": "paragonie/random_compat", @@ -1233,16 +1233,16 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.10.1", + "version": "v1.11.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "5115fa44886d1c2785d2f135ef4626db868eac4b" + "reference": "a9f968bc99485f85f9303a8524c3485a7e87bc15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/5115fa44886d1c2785d2f135ef4626db868eac4b", - "reference": "5115fa44886d1c2785d2f135ef4626db868eac4b", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a9f968bc99485f85f9303a8524c3485a7e87bc15", + "reference": "a9f968bc99485f85f9303a8524c3485a7e87bc15", "shasum": "" }, "require": { @@ -1311,20 +1311,20 @@ "secret-key cryptography", "side-channel resistant" ], - "time": "2019-07-12T16:36:59+00:00" + "time": "2019-09-12T12:05:58+00:00" }, { "name": "pelago/emogrifier", - "version": "v2.1.1", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", - "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983" + "reference": "2472bc1c3a2dee8915ecc2256139c6100024332f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983", - "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/2472bc1c3a2dee8915ecc2256139c6100024332f", + "reference": "2472bc1c3a2dee8915ecc2256139c6100024332f", "shasum": "" }, "require": { @@ -1342,7 +1342,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -1355,16 +1355,6 @@ "MIT" ], "authors": [ - { - "name": "John Reeve", - "email": "jreeve@pelagodesign.com" - }, - { - "name": "Cameron Brooks" - }, - { - "name": "Jaime Prado" - }, { "name": "Oliver Klee", "email": "github@oliverklee.de" @@ -1373,9 +1363,19 @@ "name": "Zoli Szabó", "email": "zoli.szabo+github@gmail.com" }, + { + "name": "John Reeve", + "email": "jreeve@pelagodesign.com" + }, { "name": "Jake Hotson", "email": "jake@qzdesign.co.uk" + }, + { + "name": "Cameron Brooks" + }, + { + "name": "Jaime Prado" } ], "description": "Converts CSS styles into inline style attributes in your HTML code", @@ -1385,43 +1385,40 @@ "email", "pre-processing" ], - "time": "2018-12-10T10:36:30+00:00" + "time": "2019-09-04T16:07:59+00:00" }, { "name": "php-amqplib/php-amqplib", - "version": "v2.7.3", + "version": "v2.10.1", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f" + "reference": "6e2b2501e021e994fb64429e5a78118f83b5c200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", - "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/6e2b2501e021e994fb64429e5a78118f83b5c200", + "reference": "6e2b2501e021e994fb64429e5a78118f83b5c200", "shasum": "" }, "require": { "ext-bcmath": "*", - "ext-mbstring": "*", - "php": ">=5.3.0" + "ext-sockets": "*", + "php": ">=5.6" }, "replace": { "videlalvaro/php-amqplib": "self.version" }, "require-dev": { - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "^4.8", - "scrutinizer/ocular": "^1.1", + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^5.7|^6.5|^7.0", "squizlabs/php_codesniffer": "^2.5" }, - "suggest": { - "ext-sockets": "Use AMQPSocketConnection" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.10-dev" } }, "autoload": { @@ -1447,6 +1444,11 @@ "name": "Raúl Araya", "email": "nubeiro@gmail.com", "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" } ], "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", @@ -1456,7 +1458,7 @@ "queue", "rabbitmq" ], - "time": "2018-04-30T03:54:54+00:00" + "time": "2019-10-10T13:23:40+00:00" }, { "name": "phpseclib/mcrypt_compat", @@ -1509,16 +1511,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.21", + "version": "2.0.23", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d" + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9f1287e68b3f283339a9f98f67515dd619e5bf9d", - "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c78eb5058d5bb1a183133c36d4ba5b6675dfa099", + "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099", "shasum": "" }, "require": { @@ -1552,28 +1554,28 @@ "authors": [ { "name": "Jim Wigginton", - "role": "Lead Developer", - "email": "terrafrost@php.net" + "email": "terrafrost@php.net", + "role": "Lead Developer" }, { "name": "Patrick Monnerat", - "role": "Developer", - "email": "pm@datasphere.ch" + "email": "pm@datasphere.ch", + "role": "Developer" }, { "name": "Andreas Fischer", - "role": "Developer", - "email": "bantu@phpbb.com" + "email": "bantu@phpbb.com", + "role": "Developer" }, { "name": "Hans-Jürgen Petrich", - "role": "Developer", - "email": "petrich@tronic-media.com" + "email": "petrich@tronic-media.com", + "role": "Developer" }, { "name": "Graham Campbell", - "role": "Developer", - "email": "graham@alt-three.com" + "email": "graham@alt-three.com", + "role": "Developer" } ], "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", @@ -1597,7 +1599,7 @@ "x.509", "x509" ], - "time": "2019-07-12T12:53:49+00:00" + "time": "2019-09-17T03:41:22+00:00" }, { "name": "psr/container", @@ -2079,16 +2081,16 @@ }, { "name": "symfony/css-selector", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d" + "reference": "f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/105c98bb0c5d8635bea056135304bd8edcc42b4d", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9", + "reference": "f4b3ff6a549d9ed28b2b0ecd1781bf67cf220ee9", "shasum": "" }, "require": { @@ -2128,20 +2130,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2019-01-16T21:53:39+00:00" + "time": "2019-10-02T08:36:26+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "212b020949331b6531250584531363844b34a94e" + "reference": "6229f58993e5a157f6096fc7145c0717d0be8807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/212b020949331b6531250584531363844b34a94e", - "reference": "212b020949331b6531250584531363844b34a94e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/6229f58993e5a157f6096fc7145c0717d0be8807", + "reference": "6229f58993e5a157f6096fc7145c0717d0be8807", "shasum": "" }, "require": { @@ -2198,20 +2200,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2019-06-27T06:42:14+00:00" + "time": "2019-10-01T16:40:32+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.5", + "version": "v1.1.7", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c" + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", "shasum": "" }, "require": { @@ -2256,20 +2258,20 @@ "interoperability", "standards" ], - "time": "2019-06-20T06:46:26+00:00" + "time": "2019-09-17T09:54:03+00:00" }, { "name": "symfony/filesystem", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d" + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d", - "reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263", + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263", "shasum": "" }, "require": { @@ -2306,20 +2308,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2019-06-23T08:51:25+00:00" + "time": "2019-08-20T14:07:54+00:00" }, { "name": "symfony/finder", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2" + "reference": "5e575faa95548d0586f6bedaeabec259714e44d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9638d41e3729459860bb96f6247ccb61faaa45f2", - "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2", + "url": "https://api.github.com/repos/symfony/finder/zipball/5e575faa95548d0586f6bedaeabec259714e44d1", + "reference": "5e575faa95548d0586f6bedaeabec259714e44d1", "shasum": "" }, "require": { @@ -2355,20 +2357,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-06-28T13:16:30+00:00" + "time": "2019-09-16T11:29:48+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "82ebae02209c21113908c229e9883c419720738a" + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", - "reference": "82ebae02209c21113908c229e9883c419720738a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "shasum": "" }, "require": { @@ -2380,7 +2382,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2396,13 +2398,13 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, { "name": "Gert de Pagter", "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for ctype functions", @@ -2413,20 +2415,20 @@ "polyfill", "portable" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "shasum": "" }, "require": { @@ -2438,7 +2440,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2472,20 +2474,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/process", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c" + "reference": "50556892f3cc47d4200bfd1075314139c4c9ff4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/856d35814cf287480465bb7a6c413bb7f5f5e69c", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c", + "url": "https://api.github.com/repos/symfony/process/zipball/50556892f3cc47d4200bfd1075314139c4c9ff4b", + "reference": "50556892f3cc47d4200bfd1075314139c4c9ff4b", "shasum": "" }, "require": { @@ -2521,7 +2523,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" + "time": "2019-09-26T21:17:10+00:00" }, { "name": "tedivm/jshrink", @@ -2841,16 +2843,16 @@ }, { "name": "zendframework/zend-code", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-code.git", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb" + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-code/zipball/c21db169075c6ec4b342149f446e7b7b724f95eb", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/936fa7ad4d53897ea3e3eb41b5b760828246a20b", + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b", "shasum": "" }, "require": { @@ -2858,10 +2860,10 @@ "zendframework/zend-eventmanager": "^2.6 || ^3.0" }, "require-dev": { - "doctrine/annotations": "~1.0", + "doctrine/annotations": "^1.0", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "zendframework/zend-coding-standard": "^1.0.0", + "phpunit/phpunit": "^7.5.15", + "zendframework/zend-coding-standard": "^1.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "suggest": { @@ -2884,13 +2886,13 @@ "license": [ "BSD-3-Clause" ], - "description": "provides facilities to generate arbitrary code using an object oriented interface", - "homepage": "https://github.com/zendframework/zend-code", + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", "keywords": [ + "ZendFramework", "code", - "zf2" + "zf" ], - "time": "2018-08-13T20:36:59+00:00" + "time": "2019-08-31T14:14:34+00:00" }, { "name": "zendframework/zend-config", @@ -3158,16 +3160,16 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.8.6", + "version": "1.8.7", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "20da13beba0dde8fb648be3cc19765732790f46e" + "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e", - "reference": "20da13beba0dde8fb648be3cc19765732790f46e", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/a85e67b86e9b8520d07e6415fcbcb8391b44a75b", + "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b", "shasum": "" }, "require": { @@ -3187,9 +3189,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev", - "dev-develop": "1.9.x-dev", - "dev-release-2.0": "2.0.x-dev" + "dev-release-1.8": "1.8.x-dev" } }, "autoload": { @@ -3218,20 +3218,20 @@ "psr", "psr-7" ], - "time": "2018-09-05T19:29:37+00:00" + "time": "2019-08-06T17:53:53+00:00" }, { "name": "zendframework/zend-escaper", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-escaper.git", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074" + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/31d8aafae982f9568287cb4dce987e6aff8fd074", - "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074", + "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", + "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f", "shasum": "" }, "require": { @@ -3263,7 +3263,7 @@ "escaper", "zf" ], - "time": "2018-04-25T15:48:53+00:00" + "time": "2019-09-05T20:03:20+00:00" }, { "name": "zendframework/zend-eventmanager", @@ -3384,16 +3384,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.9.1", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", - "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/d78f2cdde1c31975e18b2a0753381ed7b61118ef", + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef", "shasum": "" }, "require": { @@ -3439,26 +3439,26 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a set of commonly needed data filters", + "description": "Programmatically filter and normalize data and files", "keywords": [ "ZendFramework", "filter", "zf" ], - "time": "2018-12-17T16:00:04+00:00" + "time": "2019-08-19T07:08:04+00:00" }, { "name": "zendframework/zend-form", - "version": "2.14.1", + "version": "2.14.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-form.git", - "reference": "ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be" + "reference": "0b1616c59b1f3df194284e26f98c81ad0c377871" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-form/zipball/ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be", - "reference": "ff9385b7d0d93d9bdbc2aa4af82ab616dbc7d4be", + "url": "https://api.github.com/repos/zendframework/zend-form/zipball/0b1616c59b1f3df194284e26f98c81ad0c377871", + "reference": "0b1616c59b1f3df194284e26f98c81ad0c377871", "shasum": "" }, "require": { @@ -3523,7 +3523,7 @@ "form", "zf" ], - "time": "2019-02-26T18:13:31+00:00" + "time": "2019-10-04T10:46:36+00:00" }, { "name": "zendframework/zend-http", @@ -3582,16 +3582,16 @@ }, { "name": "zendframework/zend-hydrator", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-hydrator.git", - "reference": "70b02f4d8676e64af932625751750b5ca72fff3a" + "reference": "2bfc6845019e7b6d38b0ab5e55190244dc510285" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/70b02f4d8676e64af932625751750b5ca72fff3a", - "reference": "70b02f4d8676e64af932625751750b5ca72fff3a", + "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/2bfc6845019e7b6d38b0ab5e55190244dc510285", + "reference": "2bfc6845019e7b6d38b0ab5e55190244dc510285", "shasum": "" }, "require": { @@ -3616,10 +3616,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-release-1.0": "1.0.x-dev", - "dev-release-1.1": "1.1.x-dev", - "dev-master": "2.4.x-dev", - "dev-develop": "2.5.x-dev" + "dev-release-2.4": "2.4.x-dev" }, "zf": { "component": "Zend\\Hydrator", @@ -3641,20 +3638,20 @@ "hydrator", "zf" ], - "time": "2018-11-19T19:16:10+00:00" + "time": "2019-10-04T11:17:36+00:00" }, { "name": "zendframework/zend-i18n", - "version": "2.9.0", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" + "reference": "e17a54b3aee333ab156958f570cde630acee8b07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", - "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/e17a54b3aee333ab156958f570cde630acee8b07", + "reference": "e17a54b3aee333ab156958f570cde630acee8b07", "shasum": "" }, "require": { @@ -3662,7 +3659,7 @@ "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -3709,20 +3706,20 @@ "i18n", "zf" ], - "time": "2018-05-16T16:39:13+00:00" + "time": "2019-09-30T12:04:37+00:00" }, { "name": "zendframework/zend-inputfilter", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c" + "reference": "1f44a2e9bc394a71638b43bc7024b572fa65410e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", - "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", + "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/1f44a2e9bc394a71638b43bc7024b572fa65410e", + "reference": "1f44a2e9bc394a71638b43bc7024b572fa65410e", "shasum": "" }, "require": { @@ -3733,7 +3730,7 @@ "zendframework/zend-validator": "^2.11" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.15", "psr/http-message": "^1.0", "zendframework/zend-coding-standard": "~1.0.0" }, @@ -3766,7 +3763,7 @@ "inputfilter", "zf" ], - "time": "2019-01-30T16:58:51+00:00" + "time": "2019-08-28T19:45:32+00:00" }, { "name": "zendframework/zend-json", @@ -3825,16 +3822,16 @@ }, { "name": "zendframework/zend-loader", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-loader.git", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c" + "reference": "91da574d29b58547385b2298c020b257310898c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/78f11749ea340f6ca316bca5958eef80b38f9b6c", - "reference": "78f11749ea340f6ca316bca5958eef80b38f9b6c", + "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/91da574d29b58547385b2298c020b257310898c6", + "reference": "91da574d29b58547385b2298c020b257310898c6", "shasum": "" }, "require": { @@ -3866,20 +3863,20 @@ "loader", "zf" ], - "time": "2018-04-30T15:20:54+00:00" + "time": "2019-09-04T19:38:14+00:00" }, { "name": "zendframework/zend-log", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-log.git", - "reference": "9cec3b092acb39963659c2f32441cccc56b3f430" + "reference": "cb278772afdacb1924342248a069330977625ae6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-log/zipball/9cec3b092acb39963659c2f32441cccc56b3f430", - "reference": "9cec3b092acb39963659c2f32441cccc56b3f430", + "url": "https://api.github.com/repos/zendframework/zend-log/zipball/cb278772afdacb1924342248a069330977625ae6", + "reference": "cb278772afdacb1924342248a069330977625ae6", "shasum": "" }, "require": { @@ -3892,8 +3889,8 @@ "psr/log-implementation": "1.0.0" }, "require-dev": { - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^5.7.15 || ^6.0.8", + "mikey179/vfsstream": "^1.6.7", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.15", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.6", "zendframework/zend-escaper": "^2.5", @@ -3904,7 +3901,6 @@ "suggest": { "ext-mongo": "mongo extension to use Mongo writer", "ext-mongodb": "mongodb extension to use MongoDB writer", - "zendframework/zend-console": "Zend\\Console component to use the RequestID log processor", "zendframework/zend-db": "Zend\\Db component to use the database log writer", "zendframework/zend-escaper": "Zend\\Escaper component, for use in the XML log formatter", "zendframework/zend-mail": "Zend\\Mail component to use the email log writer", @@ -3913,8 +3909,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "zf": { "component": "Zend\\Log", @@ -3930,14 +3926,14 @@ "license": [ "BSD-3-Clause" ], - "description": "component for general purpose logging", - "homepage": "https://github.com/zendframework/zend-log", + "description": "Robust, composite logger with filtering, formatting, and PSR-3 support", "keywords": [ + "ZendFramework", "log", "logging", - "zf2" + "zf" ], - "time": "2018-04-09T21:59:51+00:00" + "time": "2019-08-23T21:28:18+00:00" }, { "name": "zendframework/zend-mail", @@ -4464,28 +4460,28 @@ }, { "name": "zendframework/zend-session", - "version": "2.8.5", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-session.git", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b" + "reference": "0a0c7ae4d8be608e30ecff714c86164ccca19ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-session/zipball/2cfd90e1a2f6b066b9f908599251d8f64f07021b", - "reference": "2cfd90e1a2f6b066b9f908599251d8f64f07021b", + "url": "https://api.github.com/repos/zendframework/zend-session/zipball/0a0c7ae4d8be608e30ecff714c86164ccca19ca3", + "reference": "0a0c7ae4d8be608e30ecff714c86164ccca19ca3", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "container-interop/container-interop": "^1.1", "mongodb/mongodb": "^1.0.1", "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", - "phpunit/phpunit": "^5.7.5 || >=6.0.13 <6.5.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-db": "^2.7", @@ -4504,8 +4500,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev", - "dev-develop": "2.9-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Session", @@ -4521,13 +4517,13 @@ "license": [ "BSD-3-Clause" ], - "description": "manage and preserve session data, a logical complement of cookie data, across multiple page requests by the same client", + "description": "Object-oriented interface to PHP sessions and storage", "keywords": [ "ZendFramework", "session", "zf" ], - "time": "2018-02-22T16:33:54+00:00" + "time": "2019-09-20T12:50:51+00:00" }, { "name": "zendframework/zend-soap", @@ -4678,16 +4674,16 @@ }, { "name": "zendframework/zend-uri", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", - "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/bfc4a5b9a309711e968d7c72afae4ac50c650083", + "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083", "shasum": "" }, "require": { @@ -4721,20 +4717,20 @@ "uri", "zf" ], - "time": "2019-02-27T21:39:04+00:00" + "time": "2019-10-07T13:35:33+00:00" }, { "name": "zendframework/zend-validator", - "version": "2.12.0", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "64c33668e5fa2d39c6289a878f927ea2b0850c30" + "reference": "7b870a7515f3a35afbecc39d63f34a861f40f58b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/64c33668e5fa2d39c6289a878f927ea2b0850c30", - "reference": "64c33668e5fa2d39c6289a878f927ea2b0850c30", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/7b870a7515f3a35afbecc39d63f34a861f40f58b", + "reference": "7b870a7515f3a35afbecc39d63f34a861f40f58b", "shasum": "" }, "require": { @@ -4788,26 +4784,26 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a set of commonly needed validators", - "homepage": "https://github.com/zendframework/zend-validator", + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", "keywords": [ + "ZendFramework", "validator", - "zf2" + "zf" ], - "time": "2019-01-30T14:26:10+00:00" + "time": "2019-10-12T12:17:57+00:00" }, { "name": "zendframework/zend-view", - "version": "2.11.2", + "version": "2.11.3", "source": { "type": "git", "url": "https://github.com/zendframework/zend-view.git", - "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2" + "reference": "e766457bd6ce13c5354e443bb949511b6904d7f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-view/zipball/4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", - "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", + "url": "https://api.github.com/repos/zendframework/zend-view/zipball/e766457bd6ce13c5354e443bb949511b6904d7f5", + "reference": "e766457bd6ce13c5354e443bb949511b6904d7f5", "shasum": "" }, "require": { @@ -4875,13 +4871,13 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a system of helpers, output filters, and variable escaping", - "homepage": "https://github.com/zendframework/zend-view", + "description": "Flexible view layer supporting and providing multiple view layers, helpers, and more", "keywords": [ + "ZendFramework", "view", - "zf2" + "zf" ], - "time": "2019-02-19T17:40:15+00:00" + "time": "2019-10-11T21:10:04+00:00" } ], "packages-dev": [ @@ -4938,25 +4934,26 @@ }, { "name": "allure-framework/allure-php-api", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-php-adapter-api.git", - "reference": "a462a0da121681577033e13c123b6cc4e89cdc64" + "url": "https://github.com/allure-framework/allure-php-commons.git", + "reference": "c7a675823ad75b8e02ddc364baae21668e7c4e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-php-adapter-api/zipball/a462a0da121681577033e13c123b6cc4e89cdc64", - "reference": "a462a0da121681577033e13c123b6cc4e89cdc64", + "url": "https://api.github.com/repos/allure-framework/allure-php-commons/zipball/c7a675823ad75b8e02ddc364baae21668e7c4e88", + "reference": "c7a675823ad75b8e02ddc364baae21668e7c4e88", "shasum": "" }, "require": { - "jms/serializer": ">=0.16.0", - "moontoast/math": ">=1.1.0", + "jms/serializer": "^0.16.0", "php": ">=5.4.0", - "phpunit/phpunit": ">=4.0.0", - "ramsey/uuid": ">=3.0.0", - "symfony/http-foundation": ">=2.0" + "ramsey/uuid": "^3.0.0", + "symfony/http-foundation": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0.0" }, "type": "library", "autoload": { @@ -4986,7 +4983,7 @@ "php", "report" ], - "time": "2016-12-07T12:15:46+00:00" + "time": "2018-05-25T14:02:11+00:00" }, { "name": "allure-framework/allure-phpunit", @@ -5097,6 +5094,99 @@ ], "time": "2019-01-16T14:22:17+00:00" }, + { + "name": "cache/cache", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/cache.git", + "reference": "902b2e5b54ea57e3a801437748652228c4c58604" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/cache/zipball/902b2e5b54ea57e3a801437748652228c4c58604", + "reference": "902b2e5b54ea57e3a801437748652228c4c58604", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.3", + "league/flysystem": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/log": "^1.0", + "psr/simple-cache": "^1.0" + }, + "conflict": { + "cache/adapter-common": "*", + "cache/apc-adapter": "*", + "cache/apcu-adapter": "*", + "cache/array-adapter": "*", + "cache/chain-adapter": "*", + "cache/doctrine-adapter": "*", + "cache/filesystem-adapter": "*", + "cache/hierarchical-cache": "*", + "cache/illuminate-adapter": "*", + "cache/memcache-adapter": "*", + "cache/memcached-adapter": "*", + "cache/mongodb-adapter": "*", + "cache/predis-adapter": "*", + "cache/psr-6-doctrine-bridge": "*", + "cache/redis-adapter": "*", + "cache/session-handler": "*", + "cache/taggable-cache": "*", + "cache/void-adapter": "*" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "defuse/php-encryption": "^2.0", + "illuminate/cache": "^5.4", + "mockery/mockery": "^0.9", + "phpunit/phpunit": "^4.0 || ^5.1", + "predis/predis": "^1.0", + "symfony/cache": "dev-master" + }, + "suggest": { + "ext-apc": "APC extension is required to use the APC Adapter", + "ext-apcu": "APCu extension is required to use the APCu Adapter", + "ext-memcache": "Memcache extension is required to use the Memcache Adapter", + "ext-memcached": "Memcached extension is required to use the Memcached Adapter", + "ext-mongodb": "Mongodb extension required to use the Mongodb adapter", + "ext-redis": "Redis extension is required to use the Redis adapter", + "mongodb/mongodb": "Mongodb lib required to use the Mongodb adapter" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cache\\": "src/" + }, + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "Library of all the php-cache adapters", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr6" + ], + "time": "2017-03-28T16:08:48+00:00" + }, { "name": "codeception/codeception", "version": "2.4.5", @@ -5190,16 +5280,16 @@ }, { "name": "codeception/phpunit-wrapper", - "version": "6.6.1", + "version": "6.7.0", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" + "reference": "93f59e028826464eac086052fa226e58967f6907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", - "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/93f59e028826464eac086052fa226e58967f6907", + "reference": "93f59e028826464eac086052fa226e58967f6907", "shasum": "" }, "require": { @@ -5232,7 +5322,7 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2019-02-26T20:47:39+00:00" + "time": "2019-08-18T15:43:35+00:00" }, { "name": "codeception/stub", @@ -5790,6 +5880,92 @@ "description": "Provides a self:update command for Symfony Console applications.", "time": "2018-10-28T01:52:03+00:00" }, + { + "name": "csharpru/vault-php", + "version": "3.5.3", + "source": { + "type": "git", + "url": "https://github.com/CSharpRU/vault-php.git", + "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/04be9776310fe7d1afb97795645f95c21e6b4fcf", + "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf", + "shasum": "" + }, + "require": { + "cache/cache": "^0.4.0", + "doctrine/inflector": "~1.1.0", + "guzzlehttp/promises": "^1.3", + "guzzlehttp/psr7": "^1.4", + "psr/cache": "^1.0", + "psr/log": "^1.0", + "weew/helpers-array": "^1.3" + }, + "require-dev": { + "codacy/coverage": "^1.1", + "codeception/codeception": "^2.2", + "csharpru/vault-php-guzzle6-transport": "~2.0", + "php-vcr/php-vcr": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Vault\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Yaroslav Lukyanov", + "email": "c_sharp@mail.ru" + } + ], + "description": "Best Vault client for PHP that you can find", + "time": "2018-04-28T04:52:17+00:00" + }, + { + "name": "csharpru/vault-php-guzzle6-transport", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/CSharpRU/vault-php-guzzle6-transport.git", + "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CSharpRU/vault-php-guzzle6-transport/zipball/33c392120ac9f253b62b034e0e8ffbbdb3513bd8", + "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.2", + "guzzlehttp/promises": "^1.3", + "guzzlehttp/psr7": "^1.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "VaultTransports\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Yaroslav Lukyanov", + "email": "c_sharp@mail.ru" + } + ], + "description": "Guzzle6 transport for Vault PHP client", + "time": "2019-03-10T06:17:37+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v1.1.0", @@ -5851,16 +6027,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.6.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24" + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/53120e0eb10355388d6ccbe462f1fea34ddadb24", - "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", + "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", "shasum": "" }, "require": { @@ -5869,12 +6045,12 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^7.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -5887,6 +6063,10 @@ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -5895,10 +6075,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -5915,40 +6091,47 @@ "docblock", "parser" ], - "time": "2019-03-25T19:12:02+00:00" + "time": "2019-10-01T18:55:10+00:00" }, { - "name": "doctrine/collections", - "version": "v1.6.2", + "name": "doctrine/cache", + "version": "v1.8.0", "source": { "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be" + "url": "https://github.com/doctrine/cache.git", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/c5e0bc17b1620e97c968ac409acbff28b8b850be", - "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "~7.1" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan-shim": "^0.9.2", + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^4.0", + "mongodb/mongodb": "^1.1", "phpunit/phpunit": "^7.0", - "vimeo/psalm": "^3.2.2" + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { "psr-4": { - "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" } }, "notification-url": "https://packagist.org/downloads/", @@ -5977,15 +6160,80 @@ "email": "schmittjoh@gmail.com" } ], - "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", - "homepage": "https://www.doctrine-project.org/projects/collections.html", + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "https://www.doctrine-project.org", "keywords": [ - "array", - "collections", - "iterators", - "php" + "cache", + "caching" + ], + "time": "2018-08-21T18:01:43+00:00" + }, + { + "name": "doctrine/inflector", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", + "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Inflector\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" ], - "time": "2019-06-09T13:48:14+00:00" + "time": "2015-11-06T14:35:42+00:00" }, { "name": "doctrine/instantiator", @@ -6104,54 +6352,8 @@ "time": "2019-06-08T11:03:04+00:00" }, { - "name": "epfremme/swagger-php", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/epfremmer/swagger-php.git", - "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/epfremmer/swagger-php/zipball/eee28a442b7e6220391ec953d3c9b936354f23bc", - "reference": "eee28a442b7e6220391ec953d3c9b936354f23bc", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.2", - "doctrine/collections": "^1.3", - "jms/serializer": "^1.1", - "php": ">=5.5", - "phpoption/phpoption": "^1.1", - "symfony/yaml": "^2.7|^3.1" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "~4.8|~5.0", - "satooshi/php-coveralls": "^1.0" - }, - "type": "package", - "autoload": { - "psr-4": { - "Epfremme\\Swagger\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Edward Pfremmer", - "email": "epfremme@nerdery.com" - } - ], - "description": "Library for parsing swagger documentation into PHP entities for use in testing and code generation", - "time": "2016-09-26T17:24:17+00:00" - }, - { - "name": "facebook/webdriver", - "version": "1.7.1", + "name": "facebook/webdriver", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/facebook/php-webdriver.git", @@ -6252,16 +6454,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.14.4", + "version": "v2.14.6", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "69ccf81f3c968be18d646918db94ab88ddf3594f" + "reference": "8d18a8bb180e2acde1c8031db09aefb9b73f6127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/69ccf81f3c968be18d646918db94ab88ddf3594f", - "reference": "69ccf81f3c968be18d646918db94ab88ddf3594f", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/8d18a8bb180e2acde1c8031db09aefb9b73f6127", + "reference": "8d18a8bb180e2acde1c8031db09aefb9b73f6127", "shasum": "" }, "require": { @@ -6291,9 +6493,10 @@ "php-cs-fixer/accessible-object": "^1.0", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^4.3" + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", + "phpunitgoodpractices/traits": "^1.5.1", + "symfony/phpunit-bridge": "^4.0", + "symfony/yaml": "^3.0 || ^4.0" }, "suggest": { "ext-mbstring": "For handling non-UTF8 characters in cache signature.", @@ -6326,17 +6529,17 @@ "MIT" ], "authors": [ - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - }, { "name": "Fabien Potencier", "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" } ], "description": "A tool to automatically fix PHP code style", - "time": "2019-06-01T10:29:34+00:00" + "time": "2019-08-31T12:47:52+00:00" }, { "name": "fzaninotto/faker", @@ -6483,6 +6686,48 @@ "description": "Expands internal property references in a yaml file.", "time": "2017-12-16T16:06:03+00:00" }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20T16:49:30+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -6575,56 +6820,44 @@ }, { "name": "jms/serializer", - "version": "1.14.0", + "version": "0.16.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca" + "reference": "c8a171357ca92b6706e395c757f334902d430ea9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", - "reference": "ee96d57024af9a7716d56fcbe3aa94b3d030f3ca", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/c8a171357ca92b6706e395c757f334902d430ea9", + "reference": "c8a171357ca92b6706e395c757f334902d430ea9", "shasum": "" }, "require": { - "doctrine/annotations": "^1.0", - "doctrine/instantiator": "^1.0.3", - "jms/metadata": "^1.3", + "doctrine/annotations": "1.*", + "jms/metadata": "~1.1", "jms/parser-lib": "1.*", - "php": "^5.5|^7.0", - "phpcollection/phpcollection": "~0.1", - "phpoption/phpoption": "^1.1" - }, - "conflict": { - "twig/twig": "<1.12" + "php": ">=5.3.2", + "phpcollection/phpcollection": "~0.1" }, "require-dev": { "doctrine/orm": "~2.1", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "phpunit/phpunit": "^4.8|^5.0", + "doctrine/phpcr-odm": "~1.0.1", + "jackalope/jackalope-doctrine-dbal": "1.0.*", "propel/propel1": "~1.7", - "psr/container": "^1.0", - "symfony/dependency-injection": "^2.7|^3.3|^4.0", - "symfony/expression-language": "^2.6|^3.0", - "symfony/filesystem": "^2.1", - "symfony/form": "~2.1|^3.0", - "symfony/translation": "^2.1|^3.0", - "symfony/validator": "^2.2|^3.0", - "symfony/yaml": "^2.1|^3.0", - "twig/twig": "~1.12|~2.0" + "symfony/filesystem": "2.*", + "symfony/form": "~2.1", + "symfony/translation": "~2.0", + "symfony/validator": "~2.0", + "symfony/yaml": "2.*", + "twig/twig": ">=1.8,<2.0-dev" }, "suggest": { - "doctrine/cache": "Required if you like to use cache functionality.", - "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", "symfony/yaml": "Required if you'd like to serialize data to YAML format." }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.14-dev" + "dev-master": "0.15-dev" } }, "autoload": { @@ -6634,16 +6867,14 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache2" ], "authors": [ { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - }, - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh", + "role": "Developer of wrapped JMSSerializerBundle" } ], "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", @@ -6655,7 +6886,7 @@ "serialization", "xml" ], - "time": "2019-04-17T08:12:16+00:00" + "time": "2014-03-18T08:39:00+00:00" }, { "name": "league/container", @@ -6722,6 +6953,90 @@ ], "time": "2017-05-10T09:20:27+00:00" }, + { + "name": "league/flysystem", + "version": "1.0.56", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "90e3f83cb10ef6b058d70f95267030e7a6236518" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/90e3f83cb10ef6b058d70f95267030e7a6236518", + "reference": "90e3f83cb10ef6b058d70f95267030e7a6236518", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.10" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2019-10-12T13:05:59+00:00" + }, { "name": "lusitanian/oauth", "version": "v0.8.11", @@ -6821,22 +7136,24 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.4.3", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c" + "reference": "5aa379674def88d1efc180d936dae1e4654c238a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", - "reference": "9e9a20fd4c77833ef41ac07eb076a7f2434ce61c", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/5aa379674def88d1efc180d936dae1e4654c238a", + "reference": "5aa379674def88d1efc180d936dae1e4654c238a", "shasum": "" }, "require": { "allure-framework/allure-codeception": "~1.3.0", "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", + "csharpru/vault-php": "~3.5.3", + "csharpru/vault-php-guzzle6-transport": "^2.0", "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", @@ -6892,27 +7209,27 @@ "magento", "testing" ], - "time": "2019-08-02T14:26:18+00:00" + "time": "2019-09-18T14:52:11+00:00" }, { "name": "mikey179/vfsstream", - "version": "v1.6.6", + "version": "v1.6.7", "source": { "type": "git", "url": "https://github.com/bovigo/vfsStream.git", - "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d" + "reference": "2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/095238a0711c974ae5b4ebf4c4534a23f3f6c99d", - "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb", + "reference": "2b544ac3a21bcc4dde5d90c4ae8d06f4319055fb", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "~4.5" + "phpunit/phpunit": "^4.5|^5.0" }, "type": "library", "extra": { @@ -6938,56 +7255,7 @@ ], "description": "Virtual file system to mock the real file system in unit tests.", "homepage": "http://vfs.bovigo.org/", - "time": "2019-04-08T13:54:32+00:00" - }, - { - "name": "moontoast/math", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/ramsey/moontoast-math.git", - "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/moontoast-math/zipball/c2792a25df5cad4ff3d760dd37078fc5b6fccc79", - "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "jakub-onderka/php-parallel-lint": "^0.9.0", - "phpunit/phpunit": "^4.7|>=5.0 <5.4", - "satooshi/php-coveralls": "^0.6.1", - "squizlabs/php_codesniffer": "^2.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Moontoast\\Math\\": "src/Moontoast/Math/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "A mathematics library, providing functionality for large numbers", - "homepage": "https://github.com/ramsey/moontoast-math", - "keywords": [ - "bcmath", - "math" - ], - "time": "2017-02-16T16:54:46+00:00" + "time": "2019-08-01T01:38:37+00:00" }, { "name": "mustache/mustache", @@ -7037,16 +7305,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.1", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", "shasum": "" }, "require": { @@ -7081,7 +7349,7 @@ "object", "object graph" ], - "time": "2019-04-07T13:18:21+00:00" + "time": "2019-08-09T12:45:53+00:00" }, { "name": "pdepend/pdepend", @@ -7326,35 +7594,33 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "~6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7376,30 +7642,30 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c" + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", + "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", "phpunit/phpunit": "^6.4" }, @@ -7427,41 +7693,40 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-04-30T17:48:53+00:00" + "time": "2019-09-12T14:27:41+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -7474,7 +7739,8 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpmd/phpmd", @@ -7596,22 +7862,22 @@ }, { "name": "phpspec/prophecy", - "version": "1.8.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", + "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -7655,7 +7921,7 @@ "spy", "stub" ], - "time": "2019-06-13T12:50:23+00:00" + "time": "2019-10-03T11:07:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8050,6 +8316,100 @@ "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.1", @@ -8263,16 +8623,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -8299,6 +8659,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -8307,17 +8671,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -8326,7 +8686,7 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/finder-facade", @@ -8751,16 +9111,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca" + "reference": "78b7611c45039e8ce81698be319851529bf040b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", - "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/78b7611c45039e8ce81698be319851529bf040b1", + "reference": "78b7611c45039e8ce81698be319851529bf040b1", "shasum": "" }, "require": { @@ -8806,20 +9166,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-06-11T15:41:59+00:00" + "time": "2019-09-10T11:25:17+00:00" }, { "name": "symfony/config", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "a17a2aea43950ce83a0603ed301bac362eb86870" + "reference": "0acb26407a9e1a64a275142f0ae5e36436342720" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/a17a2aea43950ce83a0603ed301bac362eb86870", - "reference": "a17a2aea43950ce83a0603ed301bac362eb86870", + "url": "https://api.github.com/repos/symfony/config/zipball/0acb26407a9e1a64a275142f0ae5e36436342720", + "reference": "0acb26407a9e1a64a275142f0ae5e36436342720", "shasum": "" }, "require": { @@ -8870,26 +9230,26 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2019-07-18T10:34:59+00:00" + "time": "2019-09-19T15:51:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "9ad1b83d474ae17156f6914cb81ffe77aeac3a9b" + "reference": "e1e0762a814b957a1092bff75a550db49724d05b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9ad1b83d474ae17156f6914cb81ffe77aeac3a9b", - "reference": "9ad1b83d474ae17156f6914cb81ffe77aeac3a9b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e1e0762a814b957a1092bff75a550db49724d05b", + "reference": "e1e0762a814b957a1092bff75a550db49724d05b", "shasum": "" }, "require": { "php": "^7.1.3", "psr/container": "^1.0", - "symfony/service-contracts": "^1.1.2" + "symfony/service-contracts": "^1.1.6" }, "conflict": { "symfony/config": "<4.3", @@ -8943,20 +9303,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2019-07-26T07:03:43+00:00" + "time": "2019-10-02T12:58:58+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "291397232a2eefb3347eaab9170409981eaad0e2" + "reference": "e9f7b4d19d69b133bd638eeddcdc757723b4211f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291397232a2eefb3347eaab9170409981eaad0e2", - "reference": "291397232a2eefb3347eaab9170409981eaad0e2", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/e9f7b4d19d69b133bd638eeddcdc757723b4211f", + "reference": "e9f7b4d19d69b133bd638eeddcdc757723b4211f", "shasum": "" }, "require": { @@ -9004,35 +9364,35 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-06-13T11:03:18+00:00" + "time": "2019-09-28T21:25:05+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.3.3", + "version": "v2.8.50", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8b778ee0c27731105fbf1535f51793ad1ae0ba2b" + "reference": "746f8d3638bf46ee8b202e62f2b214c3d61fb06a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8b778ee0c27731105fbf1535f51793ad1ae0ba2b", - "reference": "8b778ee0c27731105fbf1535f51793ad1ae0ba2b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/746f8d3638bf46ee8b202e62f2b214c3d61fb06a", + "reference": "746f8d3638bf46ee8b202e62f2b214c3d61fb06a", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/mime": "^4.3", - "symfony/polyfill-mbstring": "~1.1" + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php54": "~1.0", + "symfony/polyfill-php55": "~1.0" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "~3.4|~4.0" + "symfony/expression-language": "~2.4|~3.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -9059,30 +9419,24 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-07-23T11:21:36+00:00" + "time": "2019-04-16T10:00:53+00:00" }, { - "name": "symfony/mime", - "version": "v4.3.3", + "name": "symfony/options-resolver", + "version": "v4.3.5", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "6b7148029b1dd5eda1502064f06d01357b7b2d8b" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "81c2e120522a42f623233968244baebd6b36cb6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/6b7148029b1dd5eda1502064f06d01357b7b2d8b", - "reference": "6b7148029b1dd5eda1502064f06d01357b7b2d8b", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/81c2e120522a42f623233968244baebd6b36cb6a", + "reference": "81c2e120522a42f623233968244baebd6b36cb6a", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "require-dev": { - "egulias/email-validator": "^2.0", - "symfony/dependency-injection": "~3.4|^4.1" + "php": "^7.1.3" }, "type": "library", "extra": { @@ -9092,7 +9446,7 @@ }, "autoload": { "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Component\\OptionsResolver\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -9112,43 +9466,47 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A library to manipulate MIME messages", + "description": "Symfony OptionsResolver Component", "homepage": "https://symfony.com", "keywords": [ - "mime", - "mime-type" + "config", + "configuration", + "options" ], - "time": "2019-07-19T16:21:19+00:00" + "time": "2019-08-08T09:29:19+00:00" }, { - "name": "symfony/options-resolver", - "version": "v4.3.3", + "name": "symfony/polyfill-php54", + "version": "v1.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "40762ead607c8f792ee4516881369ffa553fee6f" + "url": "https://github.com/symfony/polyfill-php54.git", + "reference": "a043bcced870214922fbb4bf22679d431ec0296a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/40762ead607c8f792ee4516881369ffa553fee6f", - "reference": "40762ead607c8f792ee4516881369ffa553fee6f", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/a043bcced870214922fbb4bf22679d431ec0296a", + "reference": "a043bcced870214922fbb4bf22679d431ec0296a", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "1.12-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" + "Symfony\\Polyfill\\Php54\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -9157,54 +9515,51 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony OptionsResolver Component", + "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "config", - "configuration", - "options" + "compatibility", + "polyfill", + "portable", + "shim" ], - "time": "2019-06-13T11:01:17+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.11.0", + "name": "symfony/polyfill-php55", + "version": "v1.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af" + "url": "https://github.com/symfony/polyfill-php55.git", + "reference": "548bb39407e78e54f785b4e18c7e0d5d9e493265" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/548bb39407e78e54f785b4e18c7e0d5d9e493265", + "reference": "548bb39407e78e54f785b4e18c7e0d5d9e493265", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php72": "^1.9" - }, - "suggest": { - "ext-intl": "For best performance" + "ircmaxell/password-compat": "~1.0", + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.12-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Polyfill\\Php55\\": "" }, "files": [ "bootstrap.php" @@ -9216,38 +9571,36 @@ ], "authors": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", - "intl", "polyfill", "portable", "shim" ], - "time": "2019-03-04T13:44:35+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "bc4858fb611bda58719124ca079baff854149c89" + "reference": "54b4c428a0054e254223797d2713c31e08610831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", - "reference": "bc4858fb611bda58719124ca079baff854149c89", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/54b4c428a0054e254223797d2713c31e08610831", + "reference": "54b4c428a0054e254223797d2713c31e08610831", "shasum": "" }, "require": { @@ -9257,7 +9610,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -9293,20 +9646,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" + "reference": "04ce3335667451138df4307d6a9b61565560199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", "shasum": "" }, "require": { @@ -9315,7 +9668,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -9348,20 +9701,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/service-contracts", - "version": "v1.1.5", + "version": "v1.1.7", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d" + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffcde9615dc5bb4825b9f6aed07716f1f57faae0", + "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0", "shasum": "" }, "require": { @@ -9406,20 +9759,20 @@ "interoperability", "standards" ], - "time": "2019-06-13T11:15:36+00:00" + "time": "2019-09-17T11:12:18+00:00" }, { "name": "symfony/stopwatch", - "version": "v4.3.3", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b" + "reference": "1e4ff456bd625be5032fac9be4294e60442e9b71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/6b100e9309e8979cf1978ac1778eb155c1f7d93b", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/1e4ff456bd625be5032fac9be4294e60442e9b71", + "reference": "1e4ff456bd625be5032fac9be4294e60442e9b71", "shasum": "" }, "require": { @@ -9456,24 +9809,24 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2019-05-27T08:16:38+00:00" + "time": "2019-08-07T11:52:19+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.30", + "version": "v4.3.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "051d045c684148060ebfc9affb7e3f5e0899d40b" + "reference": "41e16350a2a1c7383c4735aa2f9fce74cf3d1178" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/051d045c684148060ebfc9affb7e3f5e0899d40b", - "reference": "051d045c684148060ebfc9affb7e3f5e0899d40b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/41e16350a2a1c7383c4735aa2f9fce74cf3d1178", + "reference": "41e16350a2a1c7383c4735aa2f9fce74cf3d1178", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -9488,7 +9841,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -9515,7 +9868,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-07-24T13:01:31+00:00" + "time": "2019-09-11T15:41:19+00:00" }, { "name": "theseer/fdomdocument", @@ -9650,16 +10003,16 @@ }, { "name": "webmozart/assert", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", "shasum": "" }, "require": { @@ -9667,8 +10020,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", "extra": { @@ -9697,7 +10049,44 @@ "check", "validate" ], - "time": "2018-12-25T11:19:39+00:00" + "time": "2019-08-24T08:43:50+00:00" + }, + { + "name": "weew/helpers-array", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/weew/helpers-array.git", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.7", + "satooshi/php-coveralls": "^0.6.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/array.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxim Kott", + "email": "maximkott@gmail.com" + } + ], + "description": "Useful collection of php array helpers.", + "time": "2016-07-21T11:18:01+00:00" } ], "aliases": [], @@ -9722,7 +10111,6 @@ "ext-pdo_mysql": "*", "ext-simplexml": "*", "ext-soap": "*", - "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", "lib-libxml": "*" diff --git a/dev/tests/acceptance/tests/_data/BB-Products.csv b/dev/tests/acceptance/tests/_data/BB-Products.csv new file mode 100644 index 0000000000000..7ab03fd5eaeda --- /dev/null +++ b/dev/tests/acceptance/tests/_data/BB-Products.csv @@ -0,0 +1,118 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +BB-D2010129,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Nero","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101772</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Nero,"Ventilatore Portatile Spray FunFan Nero","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Nero,Nero,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888101772",41,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888101772",,,,,,,,,, +BB-D2010130,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Bianco","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107965</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Bianco,"Ventilatore Portatile Spray FunFan Bianco","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Bianco,Bianco,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107965",741,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107965",,,,,,,,,, +BB-D2010131,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Rosso","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107972</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Rosso,"Ventilatore Portatile Spray FunFan Rosso","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Rosso,Rosso,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107972",570,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107972",,,,,,,,,, +BB-H1000163,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005922</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0592 Blu Marino,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0592 Blu Marino,CH0592 Blu Marino,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005922",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005922",,,,,,,,,, +BB-H1000162,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0596 Grigio","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005960</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0596 Grigio,"Sedia Pieghevole Campart Travel CH0596 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0596 Grigio,CH0596 Grigio,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005960",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005960",,,,,,,,,, +BB-F1520329,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005939</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0593 Blu Marino,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0593 Blu Marino,CH0593 Blu Marino,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005939",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005939",,,,,,,,,, +BB-F1520328,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005977</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0597 Grigio,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0597 Grigio,CH0597 Grigio,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005977",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005977",,,,,,,,,, +BB-H4502058,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Star Wars","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Star Wars""><p>I</a> fan di Star Wars non potranno fare a meno di appendere l'<strong>orologio da parete Star Wars</strong> in casa loro! Realizzato in plastica. Funziona a batterie (1 x AA, non incluse). Diametro circa: 25,5 cm. Spessore circa: 3,5 cm.</p><p> Dimenzioni per Orologio da Parete Star Wars: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 3.8 Cm</li><li>Peso: 0.287 Kg</li></ul></p><p>Codice Prodotto (EAN): 6950687214204</p>","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Star Wars""> Maggiori Informazioni</a>",0.287,1,"Taxable Goods","Catalog, Search",22.5,,,,Orologio-da-Parete-Star-Wars,"Orologio da Parete Star Wars","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica",http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=6950687214204",130,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4502058_84713.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84711.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84710.jpg","GTIN=6950687214204",,,,,,,,,, +BB-G0500195,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Rosso","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Rosso,"Braccialetto Sportivo a LED MegaLed Rosso","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Rosso,Rosso,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-G0500196,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Verde","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Verde,"Braccialetto Sportivo a LED MegaLed Verde","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Verde,Verde,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-I2500333,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Mom's Diner","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""><p>Decora</a> la tua cucina con l'originale <strong>orologio da parete</strong> <strong>Mom's Diner</strong> in stile vintage! È realizzato in legno. Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Mom's Diner: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345052</p>","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Mom's-Diner,"Orologio da Parete Mom's Diner","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno",http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,,,,,"2016-07-21 13:05:12",,,,,,,,,,,,,,,,,"GTIN=4029811345052",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500333_88060.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88059.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88058.jpg","GTIN=4029811345052",,,,,,,,,, +BB-I2500334,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Coffee Endless Cup","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""><p>Se</a> sei un appassionato di caffè, non puoi rimanere senza l'<strong>orologio da parete Coffee Endless Cup</strong>! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Coffee Endless Cup: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345069</p>","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Coffee-Endless-Cup,"Orologio da Parete Coffee Endless Cup","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa",http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,,,,,"2016-08-30 13:41:52",,,,,,,,,,,,,,,,,"GTIN=4029811345069",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500334_88064.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88063.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88062.jpg","GTIN=4029811345069",,,,,,,,,, +BB-V0000252,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Stop!","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346196</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Stop!,"Insegna Dito Vintage Look Stop!","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Stop!,Stop!,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346196",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346196",,,,,,,,,, +BB-V0000253,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Adults Only","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346202</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Adults Only,"Insegna Dito Vintage Look Adults Only","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Adults Only,Adults Only,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346202",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346202",,,,,,,,,, +BB-V0000254,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Talk","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346318</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Talk,"Insegna Dito Vintage Look Talk","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Talk,Talk,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346318",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346318",,,,,,,,,, +BB-V0000256,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Go Left","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346325</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Go Left,"Freccia Decorativa Vintage Look Go Left","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Go Left,Go Left,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346325",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346325",,,,,,,,,, +BB-V0000257,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Exit","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346332</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Exit,"Freccia Decorativa Vintage Look Exit","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Exit,Exit,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346332",19,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346332",,,,,,,,,, +BB-V0000258,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Cold beer here","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346349</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Cold beer here,"Freccia Decorativa Vintage Look Cold beer here","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Cold beer here,Cold beer here,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346349",20,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346349",,,,,,,,,, +BB-V0200190,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Bianco","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Bianco,"Ciotola in Bambù TakeTokio Bianco","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Bianco,Bianco,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200192,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Grigio","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Grigio,"Ciotola in Bambù TakeTokio Grigio","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Grigio,Grigio,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",26,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200191,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Nero","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Nero,"Ciotola in Bambù TakeTokio Nero","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Nero,Nero,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200360,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Rosa","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Rosa,"Scatola porta Tè Flower Vintage Coconut Rosa","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Rosa,Rosa,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200361,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Azzurro","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Azzurro,"Scatola porta Tè Flower Vintage Coconut Azzurro","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Azzurro,Azzurro,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200353,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Barbecue",base,"Ventilatore a Pistola classico per Barbecue BBQ Classics","<a id=""maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""><p>Utilizza</a> i migliori barbecue alimentando il fuoco con il <strong>ventilatore a pistola classico per babecue BBQ Classics</strong>! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</p><p><a href=""http://www.bbqclassics.com"" target=""_blank""><strong>www.bbqclassics.com</strong></a></p><ul><li>Realizzato in plastica e metallo</li><li>Dimensioni: 25 x 18 x 4 cm circa</li></ul><p> Dimenzioni per Ventilatore a Pistola classico per Barbecue BBQ Classics: </br><ul><li>Altezza: 5.5 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 22 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158032706</p>","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",9.3,,,,Ventilatore-a-Pistola-classico-per-Barbecue-BBQ-Classics,"Ventilatore a Pistola classico per Barbecue BBQ Classics","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Barbecue,","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria",http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,,,,,"2016-09-13 09:56:07",,,,,,,,,,,,,,,,,"GTIN=8718158032706",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200353_92696.jpg,http://dropshipping.bigbuy.eu/imgs/V0200353_92694.jpg","GTIN=8718158032706",,,,,,,,,, +BB-V1600123,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""><p>Non</a> c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente. Con il <strong>contenitore portagiochi Frozen (32 x 23 cm)</strong> sarà semplicissimo!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni aprossimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> </p><p> Dimenzioni per Contenitore Portagiochi Frozen (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766006</p>","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Frozen-(32-x-23-cm),"Contenitore Portagiochi Frozen (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente",http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766006",48,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600123_93005.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93004.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93003.jpg","GTIN=8412842766006",,,,,,,,,, +BB-V1600124,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""><p>Desideri</a> sorprendere i più piccini con un regalo molto originale? Il <strong>contenitore portagiochi Spiderman (32 x 23 cm)</strong> decorerà e metterà in ordine le loro camerette.</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni approssimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766037</p>","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Spiderman--(32-x-23-cm),"Contenitore Portagiochi Spiderman (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette",http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766037",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600124_93008.jpg,http://dropshipping.bigbuy.eu/imgs/V1600124_93007.jpg","GTIN=8412842766037",,,,,,,,,, +BB-V1600125,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""><p>Insegna</a> ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del <strong>contenitore portagiochi Frozen (45 x 32 cm)</strong>. Il <strong>portagiocattoli</strong> che tutte le bambine sognano!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Frozen (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766129</p>","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Frozen-(45-x-32-cm),"Contenitore Portagiochi Frozen (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766129",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600125_93011.jpg,http://dropshipping.bigbuy.eu/imgs/V1600125_93009.jpg","GTIN=8412842766129",,,,,,,,,, +BB-V1600126,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""><p>I</a> piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> Spiderman</strong><strong> (45 x 32 cm)</strong>. Il <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> </strong>preferito dai bambini!</p><ul><li>Fabbricato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età raccomandata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766150</p>","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Spiderman-(45-x-32-cm),"Contenitore Portagiochi Spiderman (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766150",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600126_93014.jpg,http://dropshipping.bigbuy.eu/imgs/V1600126_93013.jpg","GTIN=8412842766150",,,,,,,,,, +BB-V1300154,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Spiderman (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Spiderman (4 pezzi)""><p>Vorresti</a> sorprendere i più piccoli della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Spiderman (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Spiderman (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 30.5 Cm</li><li>Peso: 0.283 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752232</p>","Vorresti sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Spiderman (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Spiderman (4 pezzi)""> Maggiori Informazioni</a>",0.283,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Spiderman-(4-pezzi),"Zaino per Piscina Spiderman (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Vorresti sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Spiderman (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300154_93570.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300154_93570.jpg,,,,,,"2016-08-16 13:42:17",,,,,,,,,,,,,,,,,"GTIN=7569000752232",129,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300154_93576.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93575.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93574.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93573.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93572.jpg,http://dropshipping.bigbuy.eu/imgs/V1300154_93571.jpg","GTIN=7569000752232",,,,,,,,,, +BB-V1300156,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Frozen (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Frozen (4 pezzi)""><p>Vuoi</a> fare un <strong>regalo originale</strong> ai piccoli di casa? Se adorano il mare o la piscina, lo <strong>zaino per piscina Frozen (4 pezzi)</strong> li farà impazzire.</p><ul><li>Presenta una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: circa 24 x 31 x 6 cm</li></ul><p> Dimenzioni per Zaino per Piscina Frozen (4 pezzi): </br><ul><li>Altezza: 2 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 30.5 Cm</li><li>Peso: 0.277 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752225</p>","Vuoi fare un regalo originale ai piccoli di casa? Se adorano il mare o la piscina, lo zaino per piscina Frozen (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Frozen (4 pezzi)""> Maggiori Informazioni</a>",0.277,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Frozen-(4-pezzi),"Zaino per Piscina Frozen (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Vuoi fare un regalo originale ai piccoli di casa? Se adorano il mare o la piscina, lo zaino per piscina Frozen (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300156_93580.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300156_93580.jpg,,,,,,"2016-08-26 13:31:11",,,,,,,,,,,,,,,,,"GTIN=7569000752225",104,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300156_93594.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93585.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93584.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93583.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93582.jpg,http://dropshipping.bigbuy.eu/imgs/V1300156_93581.jpg","GTIN=7569000752225",,,,,,,,,, +BB-V1300157,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Minnie (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Minnie (4 pezzi)""><p>Ti</a> piacerebbe sorprendere le bimbe della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Minnie (4 pezzi)</strong> le farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Minnie (4 pezzi): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 24 Cm</li><li>Profondita': 30 Cm</li><li>Peso: 0.277 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934823796</p>","Ti piacerebbe sorprendere le bimbe della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minnie (4 pezzi) le farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Minnie (4 pezzi)""> Maggiori Informazioni</a>",0.277,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Minnie-(4-pezzi),"Zaino per Piscina Minnie (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere le bimbe della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minnie (4 pezzi) le farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300157_93587.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300157_93587.jpg,,,,,,"2016-08-26 13:30:29",,,,,,,,,,,,,,,,,"GTIN=8427934823796",137,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300157_93593.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93592.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93591.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93590.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93589.jpg,http://dropshipping.bigbuy.eu/imgs/V1300157_93586.jpg","GTIN=8427934823796",,,,,,,,,, +BB-V1300158,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Avengers (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Avengers (4 pezzi)""><p>Ti</a> piacerebbe sorprendere i più piccoli della casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Avengers (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Avengers (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 31 Cm</li><li>Peso: 0.279 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752249</p>","Ti piacerebbe sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Avengers (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Avengers (4 pezzi)""> Maggiori Informazioni</a>",0.279,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Avengers-(4-pezzi),"Zaino per Piscina Avengers (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere i più piccoli della casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Avengers (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300158_93596.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300158_93596.jpg,,,,,,"2016-08-26 11:29:45",,,,,,,,,,,,,,,,,"GTIN=7569000752249",139,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300158_93601.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93600.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93599.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93598.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93597.jpg,http://dropshipping.bigbuy.eu/imgs/V1300158_93595.jpg","GTIN=7569000752249",,,,,,,,,, +BB-V1300159,,Default,simple,"Default Category/Relax Tempo Libero,Default Category/Relax Tempo Libero/Mare e Piscina",base,"Zaino per Piscina Minions (4 pezzi)","<a id=""maggiorni_informazioni"" title=""Zaino per Piscina Minions (4 pezzi)""><p>Ti</a> piacerebbe sorprendere i più piccoli di casa con un <strong>regalo originale</strong>? Se adorano il mare o la piscina, lo <strong>zaino per piscina Minions (4 pezzi)</strong> li farà impazzire.</p><ul><li>Dispone di una cerniera, una rete posteriore e uno scompartimento per il nome</li><li>Dispone di manico regolabile</li><li>1 asciugamano 42,5 x 90,5 cm (80 % poliestere e 20 % poliammide) dall'asciugatura rapida</li><li>1 cuffia da nuoto taglia unica (85 % poliestere e 15 % elastam)</li><li>1 paio di occhialini da nuoto (norme 89/686/CEE e ISO 12312-1:2013) anti appannamento</li><li>Dimensioni dello zaino: 24 x 31 x 6 cm circa</li></ul><p> Dimenzioni per Zaino per Piscina Minions (4 pezzi): </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 23 Cm</li><li>Profondita': 31 Cm</li><li>Peso: 0.279 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934823833</p>","Ti piacerebbe sorprendere i più piccoli di casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minions (4 pezzi) li farà impazzire.</br><a href=""#maggiorni_informazioni"" title=""Zaino per Piscina Minions (4 pezzi)""> Maggiori Informazioni</a>",0.279,1,"Taxable Goods","Catalog, Search",37.9,,,,Zaino-per-Piscina-Minions-(4-pezzi),"Zaino per Piscina Minions (4 pezzi)","Relax Tempo Libero,Relax,Tempo,Libero,Mare e Piscina,Mare,Piscina,","Ti piacerebbe sorprendere i più piccoli di casa con un regalo originale? Se adorano il mare o la piscina, lo zaino per piscina Minions (4 pezzi) li farà impazzire",http://dropshipping.bigbuy.eu/imgs/V1300159_93602.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300159_93602.jpg,,,,,,"2016-08-26 11:29:10",,,,,,,,,,,,,,,,,"GTIN=8427934823833",132,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300159_93608.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93607.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93606.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93605.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93604.jpg,http://dropshipping.bigbuy.eu/imgs/V1300159_93603.jpg","GTIN=8427934823833",,,,,,,,,, +BB-G0500179,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Clip di Sicurezza a LED per Scarpe da Corsa GoFit","<a id=""maggiorni_informazioni"" title=""Clip di Sicurezza a LED per Scarpe da Corsa GoFit ""><p>Ti</a> piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua <strong>clip di sicurezza a LED per scarpe da corsa GoFit</strong>! Grazie a questo utile <strong>accessorio da corsa</strong>, sarai visibile al buio mentre corri o ti alleni. Include 2 tipi di luce (fissa o intermittente) e può essere facilmente applicata sulla parte posteriore della scarpa o indossata intorno al polso. Possiede un bottone on/off che ti permette inoltre di cambiare il tipo di luce.  <strong>Progettato in Europa</strong> con materiali di alta qualità. 1 pezzo incluso.</p><p>Caratteristiche: </p><ul><li>LED verde per alta visibilità</li><li>Circa 100 ore di luce intermittente</li><li>Circa 70 ore di luce fissa</li><li>Adattabile a scrarpe da 6 a 8,5 cm di larghezza</li><li>Funziona a batterie (2 x CR2032, incluse)</li></ul><p> Dimenzioni per Clip di Sicurezza a LED per Scarpe da Corsa GoFit : </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 3.7 Cm</li><li>Peso: 0.095 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209116</p>","Ti piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua clip di sicurezza a LED per scarpe da corsa GoFit! Grazie a questo utile accessorio da corsa, sarai visibile al buio mentre corri o ti alleni.</br><a href=""#maggiorni_informazioni"" title=""Clip di Sicurezza a LED per Scarpe da Corsa GoFit ""> Maggiori Informazioni</a>",0.095,1,"Taxable Goods","Catalog, Search",34.95,,,,Clip-di-Sicurezza-a-LED-per-Scarpe-da-Corsa-GoFit,"Clip di Sicurezza a LED per Scarpe da Corsa GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Ti piace allenarti la sera all'aria aperta? Allora, non dimenticare di indossare la tua clip di sicurezza a LED per scarpe da corsa GoFit! Grazie a questo utile accessorio da corsa, sarai visibile al buio mentre corri o ti alleni",http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit.jpg,,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit.jpg,,,,,,"2016-08-22 08:23:08",,,,,,,,,,,,,,,,,"GTIN=8018417209116",207,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_04.jpg,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_02.jpg,http://dropshipping.bigbuy.eu/imgs/clip_led_running_go_fit_002.jpg","GTIN=8018417209116",,,,,,,,,, +BB-G0500187,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Sensore di Velocità Bluetooth GoFit","<a id=""maggiorni_informazioni"" title=""Sensore di Velocità Bluetooth GoFit""><p>Se</a> sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il <strong>sensore di velocità Bluetooth GoFit</strong>, in modo da essere in grado di monitorare tutti i suoi dati in ogni momento, grazie al suo ingegnoso dispositivo! Devi solo installare il sensore e scaricare l'apposita applicazione sul tuo telefono cellulare.<br /><br />Questo <strong>sensore di velocità e ritmo </strong>è stato progettato in Europa ed è costituito di materiali resistenti all'acqua, in polimeri termoplastici. Molto semplice da installare. Compatibile con iOS (7.0 e successivi) e Android (4.3 e successivi). Funziona a batterie (1 x CR2032, incluse).</p><p>Include:</p><ul><li>1 sensore di velocità e ritmo (dimensioni: circa 8,5 x 7 x 1,5 cm)</li><li>1 magnete per ritmo pedale (dimensioni: circa 1,5 x 3,5 x 2 cm)</li><li>1 magnete per ruote</li><li>1 cacciavite</li><li>2 fascette</li><li>1 banda elastica</li></ul><p> Dimenzioni per Sensore di Velocità Bluetooth GoFit: </br><ul><li>Altezza: 20.3 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 3.5 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209109</p>","Se sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il sensore di velocità Bluetooth GoFit, in modo da essere in grado di monitorare tutti i suoi dati in ogni momento, grazie al suo ingegnoso dispositivo! Devi solo installare il sensore e scaricare l'apposita applicazione sul tuo telefono cellulare.</br><a href=""#maggiorni_informazioni"" title=""Sensore di Velocità Bluetooth GoFit""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",179.9,,,,Sensore-di-Velocità-Bluetooth-GoFit,"Sensore di Velocità Bluetooth GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Se sei un amante del ciclismo e vuoi tener traccia della velocità, ritmo e distanza mentre pedali? Allora non perdere l'occasione, acquista il sensore di velocità Bluetooth GoFit, in modo da essere in grado di monitorare tutti i suoi dati in ogni mo",http://dropshipping.bigbuy.eu/imgs/G0500187_81130.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500187_81130.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209109",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500187_81089.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81088.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81087.jpg,http://dropshipping.bigbuy.eu/imgs/G0500187_81086.jpg","GTIN=8018417209109",,,,,,,,,, +BB-G0500188,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)","<a id=""maggiorni_informazioni"" title=""Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)""><p>Da</a> ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! <span id=""result_box"" lang=""en""><span class=""hps"">Basta inserire la <strong>luce a led di sicurezza </strong></span><strong>GoFit <span class=""hps atn"">(pacco da </span><span class=""hps"">2)</span> </strong>dentro ogni scarpa da corsa per aumentare la tua visibilità.<span class=""hps""> Le loro 2 potenti luci a LED verdi si attivano ad ogni passo che fai. È davvero semplice!</span><br /><br /><span class=""hps"">Ogni luce a LED funziona a pile</span> <span class=""hps atn"">(</span>2 x <span class=""hps"">CR1220</span>, 6 <span class=""hps"">V</span>, <span class=""hps"">incluse).</span> Durata delle batterie<span class=""hps"">: </span><span class=""hps"">150,000</span> <span class=""hps"">lampeggi di luce.</span> Queste luci a LED sono costituite di materiali di alta qualità e sono adatte all'utilizzo con i lacci con uno spessore massimo di 9 mm<span class=""hps"">.</span> <span class=""hps"">Include 2 unità. Dimensioni: circa</span> <span class=""hps"">4 x</span> <span class=""hps"">1,5</span> <span class=""hps"">x</span> <span class=""hps"">0,8</span> <span class=""hps"">cm.</span><br /></span></p><p> Dimenzioni per Luce a Led di Sicurezza per Lacci GoFit (pacco da 2): </br><ul><li>Altezza: 9 Cm</li><li>Larghezza: 3.7 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.061 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209482</p>","Da ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! Basta inserire la luce a led di sicurezza GoFit (pacco da 2) dentro ogni scarpa da corsa per aumentare la tua visibilità.</br><a href=""#maggiorni_informazioni"" title=""Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)""> Maggiori Informazioni</a>",0.061,1,"Taxable Goods","Catalog, Search",33.95,,,,Luce-a-Led-di-Sicurezza-per-Lacci-GoFit-(pacco-da-2),"Luce a Led di Sicurezza per Lacci GoFit (pacco da 2)","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Da ora in poi potrai fare jogging serenamente, sapendo che puoi essere visto dai veicoli intorno a te! Basta inserire la luce a led di sicurezza GoFit (pacco da 2) dentro ogni scarpa da corsa per aumentare la tua visibilità",http://dropshipping.bigbuy.eu/imgs/G0500188_81129.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500188_81129.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209482",238,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500188_81096.jpg,http://dropshipping.bigbuy.eu/imgs/G0500188_81095.jpg","GTIN=8018417209482",,,,,,,,,, +BB-G0500189,,Default,simple,"Default Category/Sport Fitness,Default Category/Sport Fitness/Abbigliamento, Accessori e Dispositivi Indossabili",base,"Bracciale di Sicurezza LED GoFit","<a id=""maggiorni_informazioni"" title=""Bracciale di Sicurezza LED GoFit""><p>Non</a> possiedi ancora il <strong>bracciale di sicurezza LED</strong> <strong>GoFit</strong>? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo <strong>bracciale </strong>progettato in Europa. Qualunque auto o moto ti vedrà nel buio! Include 2 luci a LED e 2 modalità di illuminazione (fissa e lampeggiante) che puoi scegliere premendo il pulsante del bracciale. La cinghia di velcro lo fissa al braccio ed è flessibile e regolabile. La lunghezza massima è di circa 38,5 cm e la minima è di 31 cm. Funziona a batterie (2 x CR2023, incluse).</p><p> Dimenzioni per Bracciale di Sicurezza LED GoFit: </br><ul><li>Altezza: 9 Cm</li><li>Larghezza: 4 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.087 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209475</p>","Non possiedi ancora il bracciale di sicurezza LED GoFit? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo bracciale progettato in Europa.</br><a href=""#maggiorni_informazioni"" title=""Bracciale di Sicurezza LED GoFit""> Maggiori Informazioni</a>",0.087,1,"Taxable Goods","Catalog, Search",44.95,,,,Bracciale-di-Sicurezza-LED-GoFit,"Bracciale di Sicurezza LED GoFit","Sport Fitness,Sport,Fitness,Abbigliamento, Accessori e Dispositivi Indossabili,Abbigliamento,,Accessori,Dispositivi,Indossabili,","Non possiedi ancora il bracciale di sicurezza LED GoFit? Non perdertelo e pratica i tuoi sport preferiti con questo leggero e comodo bracciale progettato in Europa",http://dropshipping.bigbuy.eu/imgs/G0500189_81128.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500189_81128.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417209475",93,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500189_81093.jpg,http://dropshipping.bigbuy.eu/imgs/G0500189_81092.jpg,http://dropshipping.bigbuy.eu/imgs/G0500189_81091.jpg","GTIN=8018417209475",,,,,,,,,, +BB-F1510306,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Leopardato","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Leopardato,"Coperta con Maniche Snug Snug Big Tribu Leopardato","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Leopardato,Leopardato,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,,,,,,"2015-12-30 17:30:06",,,,,,,,,,,,,,,,,"GTIN=4899888103530",46,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510307,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Zebra","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Zebra,"Coperta con Maniche Snug Snug Big Tribu Zebra","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Zebra,Zebra,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,,,,,,"2016-01-21 08:26:10",,,,,,,,,,,,,,,,,"GTIN=4899888103530",486,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510308,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug Big Tribu Dalmata","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""><p>Affronta</a> il freddo invernale con l'originale<strong> coperta con<strong> maniche </strong><strong>Snug Snug <strong>Big</strong> <strong>Tribu</strong></strong>! </strong>Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche Big</strong> <strong>Tribu</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com"">www.snugsnug.com</a></strong></p><p> </p><p> </p><p> Dimenzioni per Coperta con Maniche Snug Snug Big Tribu: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 8.5 Cm</li><li>Profondita': 26.5 Cm</li><li>Peso: 0.602 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888103530</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug Big Tribu""> Maggiori Informazioni</a>",0.602,1,"Taxable Goods","Catalog, Search",39.9,,,,Coperta-con-Maniche-Snug-Snug-Big-Tribu-Dalmata,"Coperta con Maniche Snug Snug Big Tribu Dalmata","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Dalmata,Dalmata,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug Big Tribu! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle",http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510300_81364.jpg,,,,,,"2016-02-22 08:16:26",,,,,,,,,,,,,,,,,"GTIN=4899888103530",307,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510300_81361.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81365.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81363.jpg,http://dropshipping.bigbuy.eu/imgs/F1510300_81362.jpg","GTIN=4899888103530",,,,,,,,,, +BB-F1510302,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Azzurro","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Azzurro,"Coperta con Maniche Snug Snug One Big Azzurro","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Azzurro,Azzurro,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,,,,,,"2015-12-28 17:26:13",,,,,,,,,,,,,,,,,"GTIN=4899888102977",538,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510303,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Rosso","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Rosso,"Coperta con Maniche Snug Snug One Big Rosso","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Rosso,Rosso,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,,,,,,"2016-01-20 10:35:30",,,,,,,,,,,,,,,,,"GTIN=4899888102977",600,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510304,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Verde","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Verde,"Coperta con Maniche Snug Snug One Big Verde","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Verde,Verde,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,,,,,,"2015-10-06 11:20:02",,,,,,,,,,,,,,,,,"GTIN=4899888102977",764,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-F1510305,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Abbigliamento e Scarpe,Default Category/Moda Accessori/Abbigliamento e Scarpe/Pigiami e vestaglie",base,"Coperta con Maniche Snug Snug One Big Rosa","<a id=""maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""><p>Affronta</a> il freddo invernale con l'originale <strong>coperta con maniche </strong><strong>Snug Snug One Big</strong>! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti. La <strong>coperta con maniche </strong><strong>One Big</strong> è dotata di tasca centrale per avere tutto a portata di mano, il telecomando della TV, il tuo libro preferito, ecc. Realizzata in morbido pile 100 % poliestere. Misure: 170 x 130 cm circa.<br /><br /><strong><a href=""http://www.snugsnug.com/"">www.snugsnug.com</a><br /></strong></p><p> Dimenzioni per Coperta con Maniche Snug Snug One Big: </br><ul><li>Altezza: 26.5 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 23.7 Cm</li><li>Peso: 0.538 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102977</p>","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle maniche che ti permettono una totale libertà di movimenti.</br><a href=""#maggiorni_informazioni"" title=""Coperta con Maniche Snug Snug One Big""> Maggiori Informazioni</a>",0.538,1,"Taxable Goods","Catalog, Search",29.9,,,,Coperta-con-Maniche-Snug-Snug-One-Big-Rosa,"Coperta con Maniche Snug Snug One Big Rosa","Moda Accessori,Moda,Accessori,Abbigliamento e Scarpe,Abbigliamento,Scarpe,Pigiami e vestaglie,Pigiami,vestaglie,Colore Rosa,Rosa,","Affronta il freddo invernale con l'originale coperta con maniche Snug Snug One Big! Un fantastico prodotto firmato Snug Snug che ti farà sentire comodo e al caldo, mentre sei sdraiato sul divano o stai facendo qualsiasi lavoro in casa, grazie alle m",http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,,http://dropshipping.bigbuy.eu/imgs/F1510301_81358.jpg,,,,,,"2015-12-29 06:48:13",,,,,,,,,,,,,,,,,"GTIN=4899888102977",968,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/F1510301_81356.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81360.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81359.jpg,http://dropshipping.bigbuy.eu/imgs/F1510301_81357.jpg","GTIN=4899888102977",,,,,,,,,, +BB-V1300145,,Default,simple,"Default Category/Moda Accessori,Default Category/Moda Accessori/Accessori,Default Category/Relax Tempo Libero/Accessori/Ombrelli",base,"Ombrello Star Wars con LED","<a id=""maggiorni_informazioni"" title=""Ombrello Star Wars con LED""><p>I</a> fan di Guerre Stellari impazziranno con l'<strong>ombrello Star Wars con LED</strong>!</p><ul><li>Interruttore on/off sul manico</li><li>LED di vari colori sul manico centrale</li><li>Funziona a batterie (3 x AA, incluse)</li><li>Struttura: metallo, plastica e fibra di vetro</li><li>Cupola: poliestere (pongee)</li><li>Lunghezza approssimativa: 79,5 cm</li><li>Diametro approssimativo: 95 cm</li></ul><p> Dimenzioni per Ombrello Star Wars con LED: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 79.5 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.458 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000752317</p>","I fan di Guerre Stellari impazziranno con l'ombrello Star Wars con LED!Interruttore on/off sul manicoLED di vari colori sul manico centraleFunziona a batterie (3 x AA, incluse)Struttura: metallo, plastica e fibra di vetroCupola: poliestere (pongee)Lunghezza approssimativa: 79,5 cmDiametro approssimativo: 95 cm.</br><a href=""#maggiorni_informazioni"" title=""Ombrello Star Wars con LED""> Maggiori Informazioni</a>",0.458,1,"Taxable Goods","Catalog, Search",53.9,,,,Ombrello-Star-Wars-con-LED,"Ombrello Star Wars con LED","Moda Accessori,Moda,Accessori,Accessori,Ombrelli,","I fan di Guerre Stellari impazziranno con l'ombrello Star Wars con LED!Interruttore on/off sul manicoLED di vari colori sul manico centraleFunziona a batterie (3 x AA, incluse)Struttura: metallo, plastica e fibra di vetroCupola: poliestere (pongee)L",http://dropshipping.bigbuy.eu/imgs/V1300145_93662.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300145_93662.jpg,,,,,,"2016-09-12 07:48:53",,,,,,,,,,,,,,,,,"GTIN=7569000752317",23,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300145_93665.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93664.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93663.jpg,http://dropshipping.bigbuy.eu/imgs/V1300145_93661.jpg","GTIN=7569000752317",,,,,,,,,, +BB-H4530316,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Giocattoli e Giochi,Default Category/Giochi Bambini/Giocattoli e Giochi/Giochi educativi",base,"Elastici per fare bracciali con Perline di Frozen","<a id=""maggiorni_informazioni"" title=""Elastici per fare bracciali con Perline di Frozen""><p>Se</a> cerchi un <strong>gioco che intrattenga </strong>i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli <strong>elastici per fare bracciali con le perline di Frozen</strong>. Contiene 130 elastici di diversi colori, perline di differenti forme con i protagonisti di Frozen, 1 gancino metallico, una chiusura a S, perline di diversi colori, 1 strumento per tenere gli elastici. Età consigliata: +5 anni. </p><p> </p><p> Dimenzioni per Elastici per fare bracciali con Perline di Frozen: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 3.5 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8714274680036</p>","Se cerchi un gioco che intrattenga i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli elastici per fare bracciali con le perline di Frozen.</br><a href=""#maggiorni_informazioni"" title=""Elastici per fare bracciali con Perline di Frozen""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",35,,,,Elastici-per-fare-bracciali-con-Perline-di-Frozen,"Elastici per fare bracciali con Perline di Frozen","Giochi Bambini,Giochi,Bambini,Giocattoli e Giochi,Giocattoli,Giochi,Giochi educativi,Giochi,educativi,","Se cerchi un gioco che intrattenga i tuoi figli e che sia l'ideale per essere alla moda, non perdere gli elastici per fare bracciali con le perline di Frozen",http://dropshipping.bigbuy.eu/imgs/H4530316_93063.jpg,,http://dropshipping.bigbuy.eu/imgs/H4530316_93063.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8714274680036",78,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4530316_93067.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93066.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93065.jpg,http://dropshipping.bigbuy.eu/imgs/H4530316_93064.jpg","GTIN=8714274680036",,,,,,,,,, +BB-V1300134,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Cars Rosso","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Cars""><p>Vuoi</a> sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il <strong>berretto per bambini Cars</strong>. Dimensioni 54-56 cm. Misure della visiera: 14 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere</p><p> Dimenzioni per Berretto per Bambini Cars: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 17 Cm</li><li>Profondita': 21 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934797318</p>","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Cars""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",7.9,,,,Berretto-per-Bambini-Cars-Rosso,"Berretto per Bambini Cars Rosso","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Rosso,Rosso,","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars",http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934797318",87,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91197.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91196.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91195.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91194.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91193.jpg","GTIN=8427934797318",,,,,,,,,, +BB-V1300135,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Cars Nero","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Cars""><p>Vuoi</a> sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il <strong>berretto per bambini Cars</strong>. Dimensioni 54-56 cm. Misure della visiera: 14 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere</p><p> Dimenzioni per Berretto per Bambini Cars: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 17 Cm</li><li>Profondita': 21 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934797301</p>","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Cars""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",7.9,,,,Berretto-per-Bambini-Cars-Nero,"Berretto per Bambini Cars Nero","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Nero,Nero,","Vuoi sorprendere i piccoli della tua casa? Lightning McQueen proteggerà i più piccoli della casa dal sole di questa estate! Non perderti il berretto per bambini Cars",http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300133_91229.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934797301",123,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300133_91571.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91197.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91196.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91195.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91194.jpg,http://dropshipping.bigbuy.eu/imgs/V1300133_91193.jpg","GTIN=8427934797301",,,,,,,,,, +BB-V1300138,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Cappello per Bambini Batman vs Superman Blu Marino","<a id=""maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""><p>A</a> quale bambino non piace vantarsi dei suoi <strong>accessori moda</strong>? Sorprendi i più piccoli con il <strong>cappello per bambini Batman vs Superman</strong><strong> </strong>e quest'estate fai sì che siano ben protetti dai raggi solari. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65 % cotone e 35 % poliestere.</p><p> Dimenzioni per Cappello per Bambini Batman vs Superman: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 19 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934824359</p>","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari.</br><a href=""#maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",12.5,,,,Cappello-per-Bambini-Batman-vs-Superman-Blu Marino,"Cappello per Bambini Batman vs Superman Blu Marino","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Blu Marino,Blu Marino,","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari",http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934824359",98,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91201.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91200.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91199.jpg","GTIN=8427934824359",,,,,,,,,, +BB-V1300137,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Cappello per Bambini Batman vs Superman Grigio","<a id=""maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""><p>A</a> quale bambino non piace vantarsi dei suoi <strong>accessori moda</strong>? Sorprendi i più piccoli con il <strong>cappello per bambini Batman vs Superman</strong><strong> </strong>e quest'estate fai sì che siano ben protetti dai raggi solari. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65 % cotone e 35 % poliestere.</p><p> Dimenzioni per Cappello per Bambini Batman vs Superman: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 19 Cm</li><li>Profondita': 20 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934824335</p>","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari.</br><a href=""#maggiorni_informazioni"" title=""Cappello per Bambini Batman vs Superman""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",12.5,,,,Cappello-per-Bambini-Batman-vs-Superman-Grigio,"Cappello per Bambini Batman vs Superman Grigio","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Grigio,Grigio,","A quale bambino non piace vantarsi dei suoi accessori moda? Sorprendi i più piccoli con il cappello per bambini Batman vs Superman e quest'estate fai sì che siano ben protetti dai raggi solari",http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300136_91231.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934824335",102,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300136_91230.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91201.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91200.jpg,http://dropshipping.bigbuy.eu/imgs/V1300136_91199.jpg","GTIN=8427934824335",,,,,,,,,, +BB-V1300125,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Avengers Rosso","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Avengers""><p>A</a> quale bambino non piace mostrare gli <strong>accessori di moda</strong>? Sorprendili con il<strong> </strong><strong>berretto per bambini Avengers</strong> e proteggili dai raggi del sole di questa estate. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere.</p><p> Dimenzioni per Berretto per Bambini Avengers: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 18 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934792252</p>","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Avengers""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",10.9,,,,Berretto-per-Bambini-Avengers-Rosso,"Berretto per Bambini Avengers Rosso","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Rosso,Rosso,","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate",http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934792252",101,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91178.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91177.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91176.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91175.jpg","GTIN=8427934792252",,,,,,,,,, +BB-V1300126,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Berretto per Bambini Avengers Nero","<a id=""maggiorni_informazioni"" title=""Berretto per Bambini Avengers""><p>A</a> quale bambino non piace mostrare gli <strong>accessori di moda</strong>? Sorprendili con il<strong> </strong><strong>berretto per bambini Avengers</strong> e proteggili dai raggi del sole di questa estate. Taglia 52-54 cm. Dimensioni della visiera: 15 x 6,5 cm circa. Composizione: 65% cotone e 35% poliestere.</p><p> Dimenzioni per Berretto per Bambini Avengers: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 18 Cm</li><li>Peso: 0.048 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934792269</p>","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate.</br><a href=""#maggiorni_informazioni"" title=""Berretto per Bambini Avengers""> Maggiori Informazioni</a>",0.048,1,"Taxable Goods","Catalog, Search",10.9,,,,Berretto-per-Bambini-Avengers-Nero,"Berretto per Bambini Avengers Nero","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,Colore Nero,Nero,","A quale bambino non piace mostrare gli accessori di moda? Sorprendili con il berretto per bambini Avengers e proteggili dai raggi del sole di questa estate",http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300124_91180.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934792269",92,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300124_91179.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91178.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91177.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91176.jpg,http://dropshipping.bigbuy.eu/imgs/V1300124_91175.jpg","GTIN=8427934792269",,,,,,,,,, +BB-V0500179,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Stoviglie per bambini",base,"Posate Bambini Disney (5 pezzi) Minnie","<a id=""maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""><p>Quando</a> i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il <strong>set per mangiare </strong>da grandi, come le <strong>posate per bambini Disney (5 pezzi). </strong>Include: 1 piatto fondo, 1 piatto piano, 1 cucchiaio, 1 forchetta e 1 tazza.</p><ul><li>Età raccomandata: +12 mesi</li><li>Realizzato in polipropilene (senza BPA)</li><li>Adatto a lavastoviglie e microonde</li><li>Dimensioni del piatto fondo (diametro x altura): 15,5 x 3 cm circa</li><li>Dimensioni del piatto piano (diametro x altura): 22 x 1,5 cm circa</li><li>Lunghezza del cucchiaio: 15,5 cm circa</li><li>Lunghezza della forchetta: 15,5 cm circa</li><li>Dimensioni della tazza: 11 x 8,5 x 8,5 cm circa</li><li>Capacità della tazza: circa 250 ml</li><li>Conforme alla normativa UNE-EN 14372 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione: servizio di posate e utensili)</li><li>Conforme alla normativa UNE-EN 14350 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione liquida)</li></ul><p> Dimenzioni per Posate Bambini Disney (5 pezzi): </br><ul><li>Altezza: 27.5 Cm</li><li>Larghezza: 9.7 Cm</li><li>Profondita': 9 Cm</li><li>Peso: 0.487 Kg</li></ul></p><p>Codice Prodotto (EAN): 3662332013348</p>","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi).</br><a href=""#maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""> Maggiori Informazioni</a>",0.487,1,"Taxable Goods","Catalog, Search",41.5,,,,Posate-Bambini-Disney-(5-pezzi)-Minnie,"Posate Bambini Disney (5 pezzi) Minnie","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Stoviglie per bambini,Stoviglie,bambini,Modello Minnie,Minnie,","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi)",http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,,http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3662332013348",34,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,http://dropshipping.bigbuy.eu/imgs/V0500178_92049.jpg","GTIN=3662332013348",,,,,,,,,, +BB-V0500180,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Stoviglie per bambini",base,"Posate Bambini Disney (5 pezzi) Mickey","<a id=""maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""><p>Quando</a> i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il <strong>set per mangiare </strong>da grandi, come le <strong>posate per bambini Disney (5 pezzi). </strong>Include: 1 piatto fondo, 1 piatto piano, 1 cucchiaio, 1 forchetta e 1 tazza.</p><ul><li>Età raccomandata: +12 mesi</li><li>Realizzato in polipropilene (senza BPA)</li><li>Adatto a lavastoviglie e microonde</li><li>Dimensioni del piatto fondo (diametro x altura): 15,5 x 3 cm circa</li><li>Dimensioni del piatto piano (diametro x altura): 22 x 1,5 cm circa</li><li>Lunghezza del cucchiaio: 15,5 cm circa</li><li>Lunghezza della forchetta: 15,5 cm circa</li><li>Dimensioni della tazza: 11 x 8,5 x 8,5 cm circa</li><li>Capacità della tazza: circa 250 ml</li><li>Conforme alla normativa UNE-EN 14372 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione: servizio di posate e utensili)</li><li>Conforme alla normativa UNE-EN 14350 (requisiti di sicurezza per gli articoli di puericultura per l'alimentazione liquida)</li></ul><p> Dimenzioni per Posate Bambini Disney (5 pezzi): </br><ul><li>Altezza: 27.5 Cm</li><li>Larghezza: 9.7 Cm</li><li>Profondita': 9 Cm</li><li>Peso: 0.487 Kg</li></ul></p><p>Codice Prodotto (EAN): 3662332013355</p>","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi).</br><a href=""#maggiorni_informazioni"" title=""Posate Bambini Disney (5 pezzi)""> Maggiori Informazioni</a>",0.487,1,"Taxable Goods","Catalog, Search",41.5,,,,Posate-Bambini-Disney-(5-pezzi)-Mickey,"Posate Bambini Disney (5 pezzi) Mickey","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Stoviglie per bambini,Stoviglie,bambini,Modello Mickey,Mickey,","Quando i piccoli di casa hanno già familiarizzato con il fatto di mangiare da soli, bisogna fare il passo successivo e comprare loro il set per mangiare da grandi, come le posate per bambini Disney (5 pezzi)",http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,,http://dropshipping.bigbuy.eu/imgs/V0500178_92051.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3662332013355",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0500178_92050.jpg,http://dropshipping.bigbuy.eu/imgs/V0500178_92049.jpg","GTIN=3662332013355",,,,,,,,,, +BB-V1300179,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Ombrelli e cappellini per bambini",base,"Ombrello per Bambini Pieghevole Star Wars","<a id=""maggiorni_informazioni"" title=""Ombrello per Bambini Pieghevole Star Wars""><p>Ti</a> presentiamo l'ombrello più galattico del pianeta, l'<strong><strong>ombrello per bambini pieghevole Star Wars</strong></strong>! Perfetto come <strong>regalo per bambini.</strong></p><ul><li>Struttura: 75 % metallo, 25 % plastica</li><li>Cupola: 100 % poliestere</li><li>Lunghezza: circa 23-52 cm</li><li>Diametro: circa 85 cm</li><li>Custodia inclusa</li></ul><p> </p><p> Dimenzioni per Ombrello per Bambini Pieghevole Star Wars: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.255 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732739</p>","Ti presentiamo l'ombrello più galattico del pianeta, l'ombrello per bambini pieghevole Star Wars! Perfetto come regalo per bambini.</br><a href=""#maggiorni_informazioni"" title=""Ombrello per Bambini Pieghevole Star Wars""> Maggiori Informazioni</a>",0.255,1,"Taxable Goods","Catalog, Search",24.5,,,,Ombrello-per-Bambini-Pieghevole-Star-Wars,"Ombrello per Bambini Pieghevole Star Wars","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Ombrelli e cappellini per bambini,Ombrelli,cappellini,bambini,","Ti presentiamo l'ombrello più galattico del pianeta, l'ombrello per bambini pieghevole Star Wars! Perfetto come regalo per bambini",http://dropshipping.bigbuy.eu/imgs/V1300179_101280.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300179_101280.jpg,,,,,,"2016-08-29 12:58:30",,,,,,,,,,,,,,,,,"GTIN=7569000732739",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300179_101281.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101279.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101278.jpg,http://dropshipping.bigbuy.eu/imgs/V1300179_101277.jpg","GTIN=7569000732739",,,,,,,,,, +BB-V1300195,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Rubble (PAW Patrol)","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Rubble (PAW Patrol)""><p>Ti</a> presentiamo la <strong><strong>borsa termica porta merende Rubble (PAW Patrol)</strong></strong>! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Ideale per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Rubble (PAW Patrol): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732890</p>","Ti presentiamo la borsa termica porta merende Rubble (PAW Patrol)! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Rubble (PAW Patrol)""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Rubble-(PAW-Patrol),"Borsa Termica Porta Merenda Rubble (PAW Patrol)","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Ti presentiamo la borsa termica porta merende Rubble (PAW Patrol)! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300195_101259.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300195_101259.jpg,,,,,,"2016-08-26 13:36:55",,,,,,,,,,,,,,,,,"GTIN=7569000732890",59,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300195_101258.jpg,http://dropshipping.bigbuy.eu/imgs/V1300195_101257.jpg,http://dropshipping.bigbuy.eu/imgs/V1300195_101256.jpg","GTIN=7569000732890",,,,,,,,,, +BB-V1300196,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Everest (PAW Patrol)","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Everest (PAW Patrol)""><p>Scopri</a> la<strong> <strong>borsa termica porta merenda Everest (PAW Patrol)</strong></strong> che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Perfetta per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Everest (PAW Patrol): </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732906</p>","Scopri la borsa termica porta merenda Everest (PAW Patrol) che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Everest (PAW Patrol)""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Everest-(PAW-Patrol),"Borsa Termica Porta Merenda Everest (PAW Patrol)","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Scopri la borsa termica porta merenda Everest (PAW Patrol) che sta facendo furore tra i bambini! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300196_101260.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300196_101260.jpg,,,,,,"2016-08-26 13:36:36",,,,,,,,,,,,,,,,,"GTIN=7569000732906",51,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300196_101263.jpg,http://dropshipping.bigbuy.eu/imgs/V1300196_101262.jpg,http://dropshipping.bigbuy.eu/imgs/V1300196_101261.jpg","GTIN=7569000732906",,,,,,,,,, +BB-V1300198,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Neonati e Bambini,Default Category/Giochi Bambini/Neonati e Bambini/Passeggiate e viaggi",base,"Borsa Termica Porta Merenda Frozen","<a id=""maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Frozen""><p>Tutte</a> le bambine vogliono subito la <strong><strong><strong>borsa termica porta merenda</strong> Frozen</strong></strong>! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla. Perfetta per portare con sé il pranzo e la merenda.</p><ul><li>Dimensioni: circa 23 x 19 x 8 cm</li><li>Composizione: poliestere, schiuma di poliuretano e PEVA (polietilene di acetato di vinilo)</li></ul><p> Dimenzioni per Borsa Termica Porta Merenda Frozen: </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 22 Cm</li><li>Profondita': 26 Cm</li><li>Peso: 0.196 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000732920</p>","Tutte le bambine vogliono subito la borsa termica porta merenda Frozen! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla.</br><a href=""#maggiorni_informazioni"" title=""Borsa Termica Porta Merenda Frozen""> Maggiori Informazioni</a>",0.196,1,"Taxable Goods","Catalog, Search",26.9,,,,Borsa-Termica-Porta-Merenda-Frozen,"Borsa Termica Porta Merenda Frozen","Giochi Bambini,Giochi,Bambini,Neonati e Bambini,Neonati,Bambini,Passeggiate e viaggi,Passeggiate,viaggi,","Tutte le bambine vogliono subito la borsa termica porta merenda Frozen! Dispone di un disegno in rilievo nella parte frontale (gomma EVA), cerniera, manico e due cinte regolabili per appenderla",http://dropshipping.bigbuy.eu/imgs/V1300198_101268.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300198_101268.jpg,,,,,,"2016-08-30 07:32:17",,,,,,,,,,,,,,,,,"GTIN=7569000732920",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300198_101271.jpg,http://dropshipping.bigbuy.eu/imgs/V1300198_101270.jpg,http://dropshipping.bigbuy.eu/imgs/V1300198_101269.jpg","GTIN=7569000732920",,,,,,,,,, +BB-V1300204,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Materiale Scolastico,Default Category/Giochi Bambini/Materiale Scolastico/Astucci e portapenne",base,"Astuccio Scuola 3D Frozen","<a id=""maggiorni_informazioni"" title=""Astuccio Scuola 3D Frozen""><p>Le</a> piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'<strong>astuccio scuola</strong> <strong>3D Frozen</strong>!</p><ul><li>Cinque scompartimenti separati</li><li>Due cerniere</li><li>Dimensioni: 21,5 x 12 x 10 cm circa</li><li>Composizione: poliestere</li></ul><p> Dimenzioni per Astuccio Scuola 3D Frozen: </br><ul><li>Altezza: 2 Cm</li><li>Larghezza: 24 Cm</li><li>Profondita': 14 Cm</li><li>Peso: 0.099 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000733248</p>","Le piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'astuccio scuola 3D Frozen!Cinque scompartimenti separatiDue cerniereDimensioni: 21,5 x 12 x 10 cm circaComposizione: poliestere.</br><a href=""#maggiorni_informazioni"" title=""Astuccio Scuola 3D Frozen""> Maggiori Informazioni</a>",0.099,1,"Taxable Goods","Catalog, Search",19.9,,,,Astuccio-Scuola-3D-Frozen,"Astuccio Scuola 3D Frozen","Giochi Bambini,Giochi,Bambini,Materiale Scolastico,Materiale,Scolastico,Astucci e portapenne,Astucci,portapenne,","Le piccola fan delle principesse Anna ed Elsa non possono tornare a scuola senza l'astuccio scuola 3D Frozen!Cinque scompartimenti separatiDue cerniereDimensioni: 21,5 x 12 x 10 cm circaComposizione: poliestere",http://dropshipping.bigbuy.eu/imgs/V1300204_102661.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300204_102661.jpg,,,,,,"2016-09-14 07:51:11",,,,,,,,,,,,,,,,,"GTIN=7569000733248",138,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300204_102663.jpg,http://dropshipping.bigbuy.eu/imgs/V1300204_102662.jpg","GTIN=7569000733248",,,,,,,,,, +BB-V1300208,,Default,simple,"Default Category/Giochi Bambini,Default Category/Giochi Bambini/Materiale Scolastico,Default Category/Giochi Bambini/Materiale Scolastico/Zaini scuola",base,"Zaino-Sacca Frozen","<a id=""maggiorni_informazioni"" title=""Zaino-Sacca Frozen""><p>Lo</a> <strong>zaino-sacca Frozen </strong>è lo zaino che sta facendo furore tra le bambine!</p><ul><li>Realizzato in poliestere</li><li>Manico superiore e cinghie con velcro</li><li>Tasca frontale con cerniera</li><li>Dimensioni: circa 33 x 44 cm</li></ul><p> </p><p> Dimenzioni per Zaino-Sacca Frozen: </br><ul><li>Altezza: 0.5 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 46 Cm</li><li>Peso: 0.138 Kg</li></ul></p><p>Codice Prodotto (EAN): 7569000733286</p>","Lo zaino-sacca Frozen è lo zaino che sta facendo furore tra le bambine!Realizzato in poliestereManico superiore e cinghie con velcroTasca frontale con cernieraDimensioni: circa 33 x 44 cm .</br><a href=""#maggiorni_informazioni"" title=""Zaino-Sacca Frozen""> Maggiori Informazioni</a>",0.138,1,"Taxable Goods","Catalog, Search",24.9,,,,Zaino-Sacca-Frozen,"Zaino-Sacca Frozen","Giochi Bambini,Giochi,Bambini,Materiale Scolastico,Materiale,Scolastico,Zaini scuola,Zaini,scuola,","Lo zaino-sacca Frozen è lo zaino che sta facendo furore tra le bambine!Realizzato in poliestereManico superiore e cinghie con velcroTasca frontale con cernieraDimensioni: circa 33 x 44 cm ",http://dropshipping.bigbuy.eu/imgs/V1300208_102667.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300208_102667.jpg,,,,,,"2016-09-14 07:50:58",,,,,,,,,,,,,,,,,"GTIN=7569000733286",141,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300208_102669.jpg,http://dropshipping.bigbuy.eu/imgs/V1300208_102668.jpg","GTIN=7569000733286",,,,,,,,,, +BB-I4115041,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cover e custodie",base,"Custodia Impermeabile per Cellulare WpShield Azzurro","<a id=""maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""><p>Sei</a> una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la <strong>custodia impermeabile per cellulare WpShield</strong>. Con questa custodia impermeabile potrai portare il telefono ovunque ti piaccia, anche per un tuffo in piscina o sul mare.<br /><br /><a href=""http://www.waterproofshield.com/""><strong>www.waterproofshield.com</strong></a><br /><br />Questa custodia impermeabile dispone sia di un cinturino con chiusura a velcro (lunghezza massima: circa 37 cm) e un cavo con chiusura di sicurezza da indossare al collo (lunghezza massima: circa 60 cm). Composizione: PVC (Spessore: circa 3 mm). Dimensioni: circa 10,5 x 15,5 cm.</p><p> Dimenzioni per Custodia Impermeabile per Cellulare WpShield : </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 11.1 Cm</li><li>Profondita': 1.9 Cm</li><li>Peso: 0.06 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106722</p>","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield.</br><a href=""#maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""> Maggiori Informazioni</a>",0.06,1,"Taxable Goods","Catalog, Search",14.5,,,,Custodia-Impermeabile-per-Cellulare-WpShield-Azzurro,"Custodia Impermeabile per Cellulare WpShield Azzurro","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cover e custodie,Cover,custodie,Colore Azzurro,Azzurro,","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield",http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,,http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,,,,,,"2015-12-21 12:09:49",,,,,,,,,,,,,,,,,"GTIN=4899888106722",4125,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78769.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78767.jpg","GTIN=4899888106722",,,,,,,,,, +BB-I4115042,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cover e custodie",base,"Custodia Impermeabile per Cellulare WpShield Bianco","<a id=""maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""><p>Sei</a> una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la <strong>custodia impermeabile per cellulare WpShield</strong>. Con questa custodia impermeabile potrai portare il telefono ovunque ti piaccia, anche per un tuffo in piscina o sul mare.<br /><br /><a href=""http://www.waterproofshield.com/""><strong>www.waterproofshield.com</strong></a><br /><br />Questa custodia impermeabile dispone sia di un cinturino con chiusura a velcro (lunghezza massima: circa 37 cm) e un cavo con chiusura di sicurezza da indossare al collo (lunghezza massima: circa 60 cm). Composizione: PVC (Spessore: circa 3 mm). Dimensioni: circa 10,5 x 15,5 cm.</p><p> Dimenzioni per Custodia Impermeabile per Cellulare WpShield : </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 11.1 Cm</li><li>Profondita': 1.9 Cm</li><li>Peso: 0.06 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106739</p>","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield.</br><a href=""#maggiorni_informazioni"" title=""Custodia Impermeabile per Cellulare WpShield ""> Maggiori Informazioni</a>",0.06,1,"Taxable Goods","Catalog, Search",14.5,,,,Custodia-Impermeabile-per-Cellulare-WpShield-Bianco,"Custodia Impermeabile per Cellulare WpShield Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cover e custodie,Cover,custodie,Colore Bianco,Bianco,","Sei una di quelle persone che portano il loro smartphone ovunque? Se desideri che il tuo telefono venga protetto dalla sporcizia, sabbia, graffi e anche dall'acqua, non perderti la custodia impermeabile per cellulare WpShield",http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,,http://dropshipping.bigbuy.eu/imgs/I4115040_78770.jpg,,,,,,"2015-12-21 12:10:05",,,,,,,,,,,,,,,,,"GTIN=4899888106739",4321,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4115040_78768.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78769.jpg,http://dropshipping.bigbuy.eu/imgs/I4115040_78767.jpg","GTIN=4899888106739",,,,,,,,,, +BB-I4115044,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Batterie, Caricatori, Adattatori",base,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Bianco","<a id=""maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""><p>Non</a> uscire di casa senza la <strong>doppia porta USB con presa elettrica e caricabatteria da auto Pocken</strong> per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina. Questo caricatore con doppio ingresso USB è molto pratico e semplice da usare. Dimensioni approssimative: 6 x 6 x 3,5 cm.</p><p><strong><a href=""http://www.pockenrg.com"">www.pockenrg.com</a>  </strong></p><div><div>Caratteristiche tecniche:</div><ul><li>Ingresso AC: 100-240 V / 0.15 A / 50-60 Hz</li><li>Ingresso DC: 12-24 V / 0.35 A</li><li>Uscita DC: +5  V / 1 A</li></ul></div><p> Dimenzioni per Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken : </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 6.5 Cm</li><li>Peso: 0.077 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106944</p>","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina.</br><a href=""#maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""> Maggiori Informazioni</a>",0.077,1,"Taxable Goods","Catalog, Search",29.99,,,,Doppia-Porta-USB-con-Presa-Elettrica-e-Caricabatteria-da-Auto-Pocken-Bianco,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Bianco","Informatica Elettronica,Informatica,Elettronica,Batterie, Caricatori, Adattatori,Batterie,,Caricatori,,Adattatori,Colore Bianco,Bianco,","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina",http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,,,,,,"2015-05-14 07:58:09",,,,,,,,,,,,,,,,,"GTIN=4899888106944",161,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_08.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_04.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_03.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0003.jpg","GTIN=4899888106944",,,,,,,,,, +BB-I4115045,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Batterie, Caricatori, Adattatori",base,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Nero","<a id=""maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""><p>Non</a> uscire di casa senza la <strong>doppia porta USB con presa elettrica e caricabatteria da auto Pocken</strong> per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina. Questo caricatore con doppio ingresso USB è molto pratico e semplice da usare. Dimensioni approssimative: 6 x 6 x 3,5 cm.</p><p><strong><a href=""http://www.pockenrg.com"">www.pockenrg.com</a>  </strong></p><div><div>Caratteristiche tecniche:</div><ul><li>Ingresso AC: 100-240 V / 0.15 A / 50-60 Hz</li><li>Ingresso DC: 12-24 V / 0.35 A</li><li>Uscita DC: +5  V / 1 A</li></ul></div><p> Dimenzioni per Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken : </br><ul><li>Altezza: 4 Cm</li><li>Larghezza: 6.5 Cm</li><li>Profondita': 6.5 Cm</li><li>Peso: 0.077 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888106678</p>","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina.</br><a href=""#maggiorni_informazioni"" title=""Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken ""> Maggiori Informazioni</a>",0.077,1,"Taxable Goods","Catalog, Search",29.99,,,,Doppia-Porta-USB-con-Presa-Elettrica-e-Caricabatteria-da-Auto-Pocken-Nero,"Doppia Porta USB con Presa Elettrica e Caricabatteria da Auto Pocken Nero","Informatica Elettronica,Informatica,Elettronica,Batterie, Caricatori, Adattatori,Batterie,,Caricatori,,Adattatori,Colore Nero,Nero,","Non uscire di casa senza la doppia porta USB con presa elettrica e caricabatteria da auto Pocken per auto! Puoi connetterla in auto o alla rete elettrica per ricaricare i dispositivi mobili, poichè include sia un adattatore per auto che una spina",http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_00.jpg,,,,,,"2015-08-18 12:37:09",,,,,,,,,,,,,,,,,"GTIN=4899888106678",265,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0002.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_08.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_04.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0004.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_03.jpg,http://dropshipping.bigbuy.eu/imgs/cargador_usb_doble_coche_0003.jpg","GTIN=4899888106678",,,,,,,,,, +BB-I3505259,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit Bianco","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Fai</a> sport mentre ascolti la musica o parli al telefono indossando questi<strong> auricolari da corsa</strong>! Questi <strong>auricolari sportivi GoFit</strong> sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza. Progettate specificamente per gli altleti. Con materiali speciali e ottima qualità sonora. Caratteristiche:</p><ul><li>Suono: stereo</li><li>Connessione audio: cavo con uscita 3,5 mm</li><li>Microfono integrato</li><li>Pulsante di risposta alla chiamata</li><li>Risposta in frequenza: 20-20000 Hz</li><li>Intensità del suono: 93 dB</li><li>Impedenza dello speaker: 32 Ω</li><li>Adatto agli iPhone, smartphone e telefoni cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 2.7 Cm</li><li>Peso: 0.079 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417204005</p>","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.079,1,"Taxable Goods","Catalog, Search",38.9,,,,Auricolari-da-Corsa-GoFit-Bianco,"Auricolari da Corsa GoFit Bianco","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Bianco,Bianco,","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza",http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,,,,,,"2015-09-23 10:52:34",,,,,,,,,,,,,,,,,"GTIN=8018417204005",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80643.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80642.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80640.jpg","GTIN=8018417204005",,,,,,,,,, +BB-I3505260,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit Arancio","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Fai</a> sport mentre ascolti la musica o parli al telefono indossando questi<strong> auricolari da corsa</strong>! Questi <strong>auricolari sportivi GoFit</strong> sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza. Progettate specificamente per gli altleti. Con materiali speciali e ottima qualità sonora. Caratteristiche:</p><ul><li>Suono: stereo</li><li>Connessione audio: cavo con uscita 3,5 mm</li><li>Microfono integrato</li><li>Pulsante di risposta alla chiamata</li><li>Risposta in frequenza: 20-20000 Hz</li><li>Intensità del suono: 93 dB</li><li>Impedenza dello speaker: 32 Ω</li><li>Adatto agli iPhone, smartphone e telefoni cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 2.7 Cm</li><li>Peso: 0.079 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209512</p>","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.079,1,"Taxable Goods","Catalog, Search",38.9,,,,Auricolari-da-Corsa-GoFit-Arancio,"Auricolari da Corsa GoFit Arancio","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Arancio,Arancio,","Fai sport mentre ascolti la musica o parli al telefono indossando questi auricolari da corsa! Questi auricolari sportivi GoFit sono molto comodi e pratici, si adattano perfettamente al tuo orecchio per una migliore aderenza",http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505223_80644.jpg,,,,,,"2015-09-23 10:52:34",,,,,,,,,,,,,,,,,"GTIN=8018417209512",43,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505223_80641.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80643.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80642.jpg,http://dropshipping.bigbuy.eu/imgs/I3505223_80640.jpg","GTIN=8018417209512",,,,,,,,,, +BB-I3505248,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1511 Azzurro","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016015112</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1511 Azzurro,"Altoparlante Bluetooth Portatile AudioSonic SK1511 Azzurro","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1511 Azzurro,SK1511 Azzurro,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,,,,,,"2015-12-21 11:15:29",,,,,,,,,,,,,,,,,"GTIN=8713016015112",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016015112",,,,,,,,,, +BB-I3505249,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1513 Rosa","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016000996</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1513 Rosa,"Altoparlante Bluetooth Portatile AudioSonic SK1513 Rosa","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1513 Rosa,SK1513 Rosa,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,,,,,,"2015-06-01 09:01:55",,,,,,,,,,,,,,,,,"GTIN=8713016000996",16,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016000996",,,,,,,,,, +BB-I3505250,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Bluetooth Portatile AudioSonic SK1512 Verde","<a id=""maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""><p>Se</a> adori la musica e la tecnologia, l'<strong>altoparlante Bluetooth portatile AudioSonic</strong> è l'ideale per te! Riesci ad immaginare di indossare questo <strong>altoparlante</strong> intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone?<strong> </strong>Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm. Dimensioni: circa 5,5 x 7 x 1 cm. Raggio del bluetooth: circa 10 m.</p><p>Caratteristiche:</p><ul><li>Batteria ricaricabile Li-ion</li><li>Vita della batteria: 4 ore</li><li>Batteria 300 mAh</li><li>Microfono incorporato</li><li>Controllo a mani libere</li><li>Pulsanti di controllo</li><li>Porta micro USB porta: 5 V</li><li>Porta input Aux</li><li>Potenza: 3 W</li></ul><p>Include:</p><ul><li>Cavo USB + micro USB</li><li>Cavo dual jack da 3,5 mm</li></ul><p> Dimenzioni per Altoparlante Bluetooth Portatile AudioSonic: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 4.6 Cm</li><li>Peso: 0.143 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016000972</p>","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smartphone? Con questo prodotto versatile è possibile! Lunghezza della corda in silicone: circa 40 cm.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Bluetooth Portatile AudioSonic""> Maggiori Informazioni</a>",0.143,1,"Taxable Goods","Catalog, Search",33.35,,,,Altoparlante-Bluetooth-Portatile-AudioSonic-SK1512 Verde,"Altoparlante Bluetooth Portatile AudioSonic SK1512 Verde","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,Referenza e Colore SK1512 Verde,SK1512 Verde,","Se adori la musica e la tecnologia, l'altoparlante Bluetooth portatile AudioSonic è l'ideale per te! Riesci ad immaginare di indossare questo altoparlante intorno al collo mentre ascolti la tua musica preferita e rispondi alle chiamate del tuo smart",http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,,http://dropshipping.bigbuy.eu/imgs/altavoz-cordón-SK-1511.jpg,,,,,,"2016-02-01 10:38:01",,,,,,,,,,,,,,,,,"GTIN=8713016000972",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/altavoz_audiosonic_SK-1539_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_01.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_02.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_0004.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_00.jpg,http://dropshipping.bigbuy.eu/imgs/altavoz_SK-1511_04.jpg","GTIN=8713016000972",,,,,,,,,, +BB-I3505262,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Auricolari da Corsa GoFit","<a id=""maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""><p>Ora,</a> ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli <strong>auricolari da corsa GoFit</strong>! Sono davvero comodi e pratici <strong>auricolari </strong>che si adattano completamente all'orecchio, Speciale <strong>design Europeo</strong> per l'utilizzo in allenamento. Materiali e suono di alta qualità. Include una piccola custodia in tessuto per conservare gli auricolari.</p><p>Caratteristiche:</p><ul><li>Flessibili e resistenti all'acqua</li><li>Suono: stereo</li><li>Connessione audio: cavo jack 3,5 mm</li><li>Microfono incorporato</li><li>Pulsante di risposta e termine chiamata</li><li>Risposta di frequenza: 20-20000 Hz</li><li>Livello sonoro: 93 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Adatti all'uso con iPhone, smartphone e altri cellulari</li></ul><p> Dimenzioni per Auricolari da Corsa GoFit: </br><ul><li>Altezza: 20.5 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417208119</p>","Ora, ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli auricolari da corsa GoFit! Sono davvero comodi e pratici auricolari che si adattano completamente all'orecchio, Speciale design Europeo per l'utilizzo in allenamento.</br><a href=""#maggiorni_informazioni"" title=""Auricolari da Corsa GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.9,,,,Auricolari-da-Corsa-GoFit,"Auricolari da Corsa GoFit","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","Ora, ascoltare la musica o fare una chiamata non saranno più scuse valide per non allenarsi, grazie agli auricolari da corsa GoFit! Sono davvero comodi e pratici auricolari che si adattano completamente all'orecchio, Speciale design Europeo per l'ut",http://dropshipping.bigbuy.eu/imgs/I3505262_80647.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505262_80647.jpg,,,,,,"2016-09-07 15:19:36",,,,,,,,,,,,,,,,,"GTIN=8018417208119",46,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505262_80646.jpg,http://dropshipping.bigbuy.eu/imgs/I3505262_80645.jpg","GTIN=8018417208119",,,,,,,,,, +BB-G0500185,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Fascia Sportiva con Auricolari GoFit Verde","<a id=""maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""><p>Se</a> adori gli sport e ti piace tenerti informato con gli ultimi <strong>accessori sportivi</strong>, non puoi perderti questa ottima <strong>fascia sportiva con auricolari GoFit</strong>. Con questa fascia per la testa, potrai ascoltare i tuoi brani musicali preferiti mentre fai jogging, inoltre potrai rispondere alle chiamate, semplicemente premendo il pulsante di risposta e chiusura chiamate incorporato nella doppia connessione, con cavo audio jack da 3,5 mm (lunghezza: circa 1 m). Altoparlanti rimovibili per permetterti di lavare la fascia. Ampiezza: circa 9,5 cm. Diametro: circa 27 cm. Esterno 100% poliestere. Interno in microfibra polare.<br /><br />Caratteristiche:</p><ul><li>Sensibilità: 5 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Frequenza: 20 Hz-20 kHz</li><li>Adatto a smartphone e altri telefoni mobili</li></ul><p> Dimenzioni per Fascia Sportiva con Auricolari GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4.5 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209635</p>","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit.</br><a href=""#maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.99,,,,Fascia-Sportiva-con-Auricolari-GoFit-Verde,"Fascia Sportiva con Auricolari GoFit Verde","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Verde,Verde,","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit",http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,,,,,,"2015-09-28 07:07:26",,,,,,,,,,,,,,,,,"GTIN=8018417209635",11,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81117.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81116.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81115.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81114.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81113.jpg","GTIN=8018417209635",,,,,,,,,, +BB-G0500186,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Fascia Sportiva con Auricolari GoFit Arancio","<a id=""maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""><p>Se</a> adori gli sport e ti piace tenerti informato con gli ultimi <strong>accessori sportivi</strong>, non puoi perderti questa ottima <strong>fascia sportiva con auricolari GoFit</strong>. Con questa fascia per la testa, potrai ascoltare i tuoi brani musicali preferiti mentre fai jogging, inoltre potrai rispondere alle chiamate, semplicemente premendo il pulsante di risposta e chiusura chiamate incorporato nella doppia connessione, con cavo audio jack da 3,5 mm (lunghezza: circa 1 m). Altoparlanti rimovibili per permetterti di lavare la fascia. Ampiezza: circa 9,5 cm. Diametro: circa 27 cm. Esterno 100% poliestere. Interno in microfibra polare.<br /><br />Caratteristiche:</p><ul><li>Sensibilità: 5 dB</li><li>Impedenza altoparlante: 32 Ω</li><li>Frequenza: 20 Hz-20 kHz</li><li>Adatto a smartphone e altri telefoni mobili</li></ul><p> Dimenzioni per Fascia Sportiva con Auricolari GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 9 Cm</li><li>Profondita': 4.5 Cm</li><li>Peso: 0.102 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417209505</p>","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit.</br><a href=""#maggiorni_informazioni"" title=""Fascia Sportiva con Auricolari GoFit""> Maggiori Informazioni</a>",0.102,1,"Taxable Goods","Catalog, Search",49.99,,,,Fascia-Sportiva-con-Auricolari-GoFit-Arancio,"Fascia Sportiva con Auricolari GoFit Arancio","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,Colore Arancio,Arancio,","Se adori gli sport e ti piace tenerti informato con gli ultimi accessori sportivi, non puoi perderti questa ottima fascia sportiva con auricolari GoFit",http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500184_81118.jpg,,,,,,"2015-09-28 07:07:51",,,,,,,,,,,,,,,,,"GTIN=8018417209505",32,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500184_81112.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81117.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81116.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81115.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81114.jpg,http://dropshipping.bigbuy.eu/imgs/G0500184_81113.jpg","GTIN=8018417209505",,,,,,,,,, +BB-I3505265,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Casse",base,"Altoparlante Sportivo Bluetooth GoFit","<a id=""maggiorni_informazioni"" title=""Altoparlante Sportivo Bluetooth GoFit""><p>Sei</a> un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso <strong>altoparlante sportivo Bluetooth GoFit</strong> ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica preferita durante le tue uscite, così come rispondere alle chiamate attraverso la funzione Bluetooth. Resistente all'acqua. Include un cavo di ricarica USB. L'altoparlante ha una clip per inserirlo su una cintura e una banda elastica per indossarlo al polso, nello zaino, ecc. Misure (diametro x altezza) circa: 8,5 cm x 3 cm. Peso: circa 159 g.</p><p>Caratteristiche:</p><ul><li>Protezione impermeabile: IP4</li><li>Bluetooth 2.0 + EDR: distanza fino a circa 10 m</li><li>Funzione mani libere</li><li>Permette la ricezione di chiamate e terminarle</li><li>Pulsanti di pausa, avanzamento e indietro</li><li>Tempo di riproduzione: circa 2,5 ore</li><li>Frequenza: 90 Hz-20 KHz</li><li>Uscita altoparlante: 5 W</li><li>Connessione per audio jack<em> </em>3,5 mm</li></ul><p> </p><p> Dimenzioni per Altoparlante Sportivo Bluetooth GoFit: </br><ul><li>Altezza: 20 Cm</li><li>Larghezza: 6 Cm</li><li>Profondita': 9.5 Cm</li><li>Peso: 0.278 Kg</li></ul></p><p>Codice Prodotto (EAN): 8018417207747</p>","Sei un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso altoparlante sportivo Bluetooth GoFit ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica preferita durante le tue uscite, così come rispondere alle chiamate attraverso la funzione Bluetooth.</br><a href=""#maggiorni_informazioni"" title=""Altoparlante Sportivo Bluetooth GoFit""> Maggiori Informazioni</a>",0.278,1,"Taxable Goods","Catalog, Search",89.9,,,,Altoparlante-Sportivo-Bluetooth-GoFit,"Altoparlante Sportivo Bluetooth GoFit","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Casse,","Sei un amante dello sport e adori fare escursioni in montagna? Ti piace la musica? Allora questo prodotto ti starà a pennello! Questo favoloso altoparlante sportivo Bluetooth GoFit ti accompagnerà dove vuoi, permettendoti di ascoltare la tua musica ",http://dropshipping.bigbuy.eu/imgs/I3505265_81327.jpg,,http://dropshipping.bigbuy.eu/imgs/I3505265_81327.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8018417207747",10,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I3505265_81169.jpg,http://dropshipping.bigbuy.eu/imgs/I3505265_81168.jpg,http://dropshipping.bigbuy.eu/imgs/I3505265_81167.jpg","GTIN=8018417207747",,,,,,,,,, +BB-H1000173,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Azzurro","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109204</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Azzurro,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Azzurro","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Azzurro,Azzurro,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,,,,,,"2016-02-25 15:50:21",,,,,,,,,,,,,,,,,"GTIN=4899888109204",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109204",,,,,,,,,, +BB-H1000174,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Nero","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109273</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Nero,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Nero","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Nero,Nero,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,,,,,,"2016-02-25 15:50:21",,,,,,,,,,,,,,,,,"GTIN=4899888109273",131,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109273",,,,,,,,,, +BB-H1000182,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Scooter Elettrici",base,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Grafitti","<a id=""maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""><p>Dimentica</a> i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà. Metti alla prova il tuo equilibrio e divertiti!</p><p><br /><strong><a href=""http://www.roverdroid.com/"">www.roverdroid.com</a></strong></p><p>Caratteristiche:</p><ul><li>Batteria a litio: 160 Wh</li><li>Pulsante on/off</li><li>Indicatore luminoso del livello di carica sull'asse centrale</li><li>Illuminazione di segnaletica anteriore LED</li><li>Zona d'appoggio in gomma resistente e antiscivolo</li><li>Velocità massima: 10 km/h circa</li><li>Autonomia con carica completa: da 17 a 20 km circa</li><li>Peso: 10 kg circa</li><li>Tempo di carica: 3 ore circa</li><li>Peso massimo supportato: 100 kg</li><li>Dimensioni: 58 x 18 x 18 cm circa</li><li>Diametro delle ruote: 7""</li><li>Borsa di trasporto e caricabatterie inclusi (CA: 100-240 V, 50-60 Hz, 1,2 A / DC: 36 V, 2 A)</li></ul><p> Dimenzioni per Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid: </br><ul><li>Altezza: 22.5 Cm</li><li>Larghezza: 64 Cm</li><li>Profondita': 24 Cm</li><li>Peso: 11.85 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109747</p>","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spostarsi che chiunque noterà.</br><a href=""#maggiorni_informazioni"" title=""Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid""> Maggiori Informazioni</a>",11.85,1,"Taxable Goods","Catalog, Search",599,,,,Mini-Scooter-Elettrico-di-Auto-Equilibrio-(2-ruote)-Rover-Droid-Grafitti,"Mini Scooter Elettrico di Auto Equilibrio (2 ruote) Rover Droid Grafitti","Informatica Elettronica,Informatica,Elettronica,Scooter Elettrici,Scooter,Elettrici,Colore Grafitti,Grafitti,","Dimentica i noiosi trasporti convenzionali e diventa il re delle strade con il mini scooter elettrico di auto equilibrio (2 ruote) Rover Droid! Questo pratico, intelligente ed originale veicolo motorizzato costituisce un modo rapido e comodo di spos",http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,,http://dropshipping.bigbuy.eu/imgs/H1000172_90270.jpg,,,,,,"2016-02-25 16:33:57",,,,,,,,,,,,,,,,,"GTIN=4899888109747",92,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H1000172_90268.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_90282.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87995.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87979.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87978.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87976.jpg,http://dropshipping.bigbuy.eu/imgs/H1000172_87975.jpg","GTIN=4899888109747",,,,,,,,,, +BB-I2500322,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Nero","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109242</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Nero,"Orologio Intelligente Smartwatch BT110 con Audio Nero","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Nero,Nero,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109242",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109242",,,,,,,,,, +BB-I2500323,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Bianco","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109259</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Bianco,"Orologio Intelligente Smartwatch BT110 con Audio Bianco","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Bianco,Bianco,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109259",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109259",,,,,,,,,, +BB-I2500324,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Smartwatch",base,"Orologio Intelligente Smartwatch BT110 con Audio Rosso","<a id=""maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""><p>Sfoggia</a> il tuo <strong>orologio intelligente Smartwatch BT110 con audio</strong>! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro. Inoltre, questo orologio dispone di varie funzioni autonome: allarme, calendario, calcolatrice, controllo del sonno...</p><p><a href=""http://www.bitblin.com/"" target=""_blank""><strong>www.bitblin.com</strong></a><br /> <br />Caratteristiche:</p><ul><li>Schermo touch</li><li>Risoluzione: 128 x 128 pixel</li><li>Batteria a litio: 3,7 V / 230 mA</li><li>Durata appross. in attesa: 150 h</li><li>Durata appross. in conversazione: 3 h</li><li>Connessione micro USB</li><li>Bluetooth 3.0</li><li>Vibrazione</li><li>Dimensioni appross. della sfera: 4 x 4,5 x 1 cm</li><li>Cavo USB da micro USB incluso</li><li>Il menu è disponibile in spagnolo, inglese, francese, danese, polacco, portoghese, italiano, tedesco, turco, russo e svedese.</li><li>Compatibile con smartphones Android</li></ul><p> </p><p>Con Smartphone Android da 4.0 a 5.0 è possibile ricevere notifiche di SMS, e-mails, WhatsApp e social network (scaricando l'applicazione indicata nella pagina web del prodotto).</p><p> Dimenzioni per Orologio Intelligente Smartwatch BT110 con Audio: </br><ul><li>Altezza: 11 Cm</li><li>Larghezza: 8 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.145 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888109266</p>","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usarlo come cronometro, barometro, altimetro e podometro.</br><a href=""#maggiorni_informazioni"" title=""Orologio Intelligente Smartwatch BT110 con Audio""> Maggiori Informazioni</a>",0.145,1,"Taxable Goods","Catalog, Search",94.9,,,,Orologio-Intelligente-Smartwatch-BT110-con-Audio-Rosso,"Orologio Intelligente Smartwatch BT110 con Audio Rosso","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Smartwatch,Colore Rosso,Rosso,","Sfoggia il tuo orologio intelligente Smartwatch BT110 con audio! Sincronizzalo tramite Bluetooth al tuo smartphone e ti permette di realizzare e rispondere alle telefonate, accedere all'agenda e alla cronologia delle chiamate, ascoltare musica e usa",http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500321_101195.jpg,,,,,,"2016-02-11 08:36:39",,,,,,,,,,,,,,,,,"GTIN=4899888109266",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500321_84125.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101196.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101194.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_101193.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84129.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84128.jpg,http://dropshipping.bigbuy.eu/imgs/I2500321_84127.jpg","GTIN=4899888109266",,,,,,,,,, +BB-I4110022,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Smartphone MyWigo UNO 5'' Bianco","<a id=""maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""><p>Acquista</a> uno dei migliori <strong>telefoni cellulari sbloccati</strong> sul mercato in questo momento, lo <strong>smartphone MyWigo UNO</strong> <strong>5''</strong>! Include caricabatterie e cavetto USB al cavetto micro USB.</p><p>Caratteristiche:</p><ul><li>Schermo Vetro Ricurvo 5'' HD IPS 2.5D </li><li>Batteria a polimeri di litio: 2350 mAh</li><li>Fotocamera anteriore: 5 Mpx</li><li>Telecamera posteriore: 13 Mpx Sony, IMX 214 sensore con autofocus e flash LED</li><li>Processore: MTK6753 Octa Core a 1.3 GHz di 64 Bit</li><li>RAM: 2 GB DDR</li><li>Memoria interna: 32 GB (16 GB + 16 GB micro SD)</li><li>Dual SIM</li><li>Sistema operativo: Android Lollipop 5.1</li><li>Wi-Fi</li><li>Bluetooth 4.0 + HS</li><li>GPS</li><li>2G: GSM a 850/900/1800/1900 MHz</li><li>3G: WCDMA 900/2100 MHz</li><li>Tecnologia 4G (LTE) FDD 800/1800/2100/2600 MHz</li><li>Caricabatterie: AC 110-240 V, DC 5 V, 1000 mA</li><li>Dimensioni: 7 x 14 x 0,8 cm circa</li><li>Dimensioni dello schermo: 6 x 11 cm circa</li><li>Peso: 138 gr circa</li></ul><p> Dimenzioni per Smartphone MyWigo UNO 5'' : </br><ul><li>Altezza: 10.5 Cm</li><li>Larghezza: 18 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.347 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436533839565</p>","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB.</br><a href=""#maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""> Maggiori Informazioni</a>",0.347,1,"Taxable Goods","Catalog, Search",412.5,,,,Smartphone-MyWigo-UNO-5''-Bianco,"Smartphone MyWigo UNO 5'' Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB",http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,,http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,,,,,,"2015-12-16 14:44:27",,,,,,,,,,,,,,,,,"GTIN=8436533839565",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87812.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87811.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87810.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87808.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87807.jpg","GTIN=8436533839565",,,,,,,,,, +BB-I4110023,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Smartphone MyWigo UNO 5'' Nero","<a id=""maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""><p>Acquista</a> uno dei migliori <strong>telefoni cellulari sbloccati</strong> sul mercato in questo momento, lo <strong>smartphone MyWigo UNO</strong> <strong>5''</strong>! Include caricabatterie e cavetto USB al cavetto micro USB.</p><p>Caratteristiche:</p><ul><li>Schermo Vetro Ricurvo 5'' HD IPS 2.5D </li><li>Batteria a polimeri di litio: 2350 mAh</li><li>Fotocamera anteriore: 5 Mpx</li><li>Telecamera posteriore: 13 Mpx Sony, IMX 214 sensore con autofocus e flash LED</li><li>Processore: MTK6753 Octa Core a 1.3 GHz di 64 Bit</li><li>RAM: 2 GB DDR</li><li>Memoria interna: 32 GB (16 GB + 16 GB micro SD)</li><li>Dual SIM</li><li>Sistema operativo: Android Lollipop 5.1</li><li>Wi-Fi</li><li>Bluetooth 4.0 + HS</li><li>GPS</li><li>2G: GSM a 850/900/1800/1900 MHz</li><li>3G: WCDMA 900/2100 MHz</li><li>Tecnologia 4G (LTE) FDD 800/1800/2100/2600 MHz</li><li>Caricabatterie: AC 110-240 V, DC 5 V, 1000 mA</li><li>Dimensioni: 7 x 14 x 0,8 cm circa</li><li>Dimensioni dello schermo: 6 x 11 cm circa</li><li>Peso: 138 gr circa</li></ul><p> Dimenzioni per Smartphone MyWigo UNO 5'' : </br><ul><li>Altezza: 10.5 Cm</li><li>Larghezza: 18 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.347 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436533839558</p>","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB.</br><a href=""#maggiorni_informazioni"" title=""Smartphone MyWigo UNO 5'' ""> Maggiori Informazioni</a>",0.347,1,"Taxable Goods","Catalog, Search",412.5,,,,Smartphone-MyWigo-UNO-5''-Nero,"Smartphone MyWigo UNO 5'' Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Acquista uno dei migliori telefoni cellulari sbloccati sul mercato in questo momento, lo smartphone MyWigo UNO 5''! Include caricabatterie e cavetto USB al cavetto micro USB",http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,,http://dropshipping.bigbuy.eu/imgs/I4110021_87813.jpg,,,,,,"2015-12-16 14:44:27",,,,,,,,,,,,,,,,,"GTIN=8436533839558",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I4110021_87809.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87812.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87811.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87810.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87808.jpg,http://dropshipping.bigbuy.eu/imgs/I4110021_87807.jpg","GTIN=8436533839558",,,,,,,,,, +BB-V1400101,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Tlink11 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""><p>Se</a> sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il <strong>telefono cellulare </strong><strong>Thomson Tlink11 </strong>è ciò che stai cercando!</p><p>Caratteristiche:</p><ul><li>Telefono cellulare sbloccato</li><li>Schermo: 1.77"" 128 x 160, colori 65 K</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Dual SIM</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Connessione per auricolari (non inclusi)</li><li>Lista contatti: 200 nomi</li><li>Funzione SMS</li><li>Mani-libere</li><li>Batteria Li-ion 600 mAh</li><li>Include: batteria e caricabatterie</li><li>Dimensioni: 4.5 x 11 x 1 cm circa </li></ul><p> Dimenzioni per Telefono Cellulare Thomson Tlink11: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570047688</p>","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSchermo: 1.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",43.5,,,,Telefono-Cellulare-Thomson-Tlink11-Bianco,"Telefono Cellulare Thomson Tlink11 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSche",http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570047688",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,http://dropshipping.bigbuy.eu/imgs/V1400100_91203.jpg","GTIN=3527570047688",,,,,,,,,, +BB-V1400102,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Tlink11 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""><p>Se</a> sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il <strong>telefono cellulare </strong><strong>Thomson Tlink11 </strong>è ciò che stai cercando!</p><p>Caratteristiche:</p><ul><li>Telefono cellulare sbloccato</li><li>Schermo: 1.77"" 128 x 160, colori 65 K</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Dual SIM</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Connessione per auricolari (non inclusi)</li><li>Lista contatti: 200 nomi</li><li>Funzione SMS</li><li>Mani-libere</li><li>Batteria Li-ion 600 mAh</li><li>Include: batteria e caricabatterie</li><li>Dimensioni: 4.5 x 11 x 1 cm circa </li></ul><p> Dimenzioni per Telefono Cellulare Thomson Tlink11: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 5 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570047596</p>","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSchermo: 1.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Tlink11""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",43.5,,,,Telefono-Cellulare-Thomson-Tlink11-Nero,"Telefono Cellulare Thomson Tlink11 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Se sei alla ricerca di un telefono cellulare user-friendly per le persone anziane o per chi è agli inizi nel mondo della telefonia mobile, il telefono cellulare Thomson Tlink11 è ciò che stai cercando!Caratteristiche:Telefono cellulare sbloccatoSche",http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400100_91204.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570047596",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400100_91205.jpg,http://dropshipping.bigbuy.eu/imgs/V1400100_91203.jpg","GTIN=3527570047596",,,,,,,,,, +BB-V1400104,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Serea51 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""><p>In</a> arrivo il <strong>telefono cellulare</strong><strong> Thomson Serea51</strong><strong> </strong>per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!</p><p>Caratteristiche:</p><ul><li>Cellulare sbloccato</li><li>Schermo: 1,77"" 160 x 128, 65 K colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Tasti di grandi dimensioni</li><li>Luce LED</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 220 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5 x 11 x 1,4 cm circa</li></ul><p> Dimenzioni per Telefono Cellulare Thomson Serea51: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 11.5 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.288 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046964</p>","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1,77" 160 x 128, 65 K coloriReti: GSM-EDGE 850/900/1800/1900 MHzTasto per le chiamate d'emergenzaTasti di grandi dimensioniLuce LEDFotocamera VGABluetoothMP3Radio FMMicro USBMicro SD (scheda non inclusa)Agenda: 250 vociFunzione SMS, MMSkit auricolare mani libereBatteria Li-ion 800 mAhDurata della batteria: 220 h in standby / 5,5 h in conversazioneInclude: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolariDimensioni (senza base): 5 x 11 x 1,4 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""> Maggiori Informazioni</a>",0.288,1,"Taxable Goods","Catalog, Search",85.9,,,,Telefono-Cellulare-Thomson-Serea51-Bianco,"Telefono Cellulare Thomson Serea51 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1",http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046964",42,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,http://dropshipping.bigbuy.eu/imgs/V1400103_91206.jpg","GTIN=3527570046964",,,,,,,,,, +BB-V1400105,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellulare Thomson Serea51 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""><p>In</a> arrivo il <strong>telefono cellulare</strong><strong> Thomson Serea51</strong><strong> </strong>per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!</p><p>Caratteristiche:</p><ul><li>Cellulare sbloccato</li><li>Schermo: 1,77"" 160 x 128, 65 K colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Tasti di grandi dimensioni</li><li>Luce LED</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Micro USB</li><li>Micro SD (scheda non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 220 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5 x 11 x 1,4 cm circa</li></ul><p> Dimenzioni per Telefono Cellulare Thomson Serea51: </br><ul><li>Altezza: 18 Cm</li><li>Larghezza: 11.5 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.288 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046186</p>","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1,77" 160 x 128, 65 K coloriReti: GSM-EDGE 850/900/1800/1900 MHzTasto per le chiamate d'emergenzaTasti di grandi dimensioniLuce LEDFotocamera VGABluetoothMP3Radio FMMicro USBMicro SD (scheda non inclusa)Agenda: 250 vociFunzione SMS, MMSkit auricolare mani libereBatteria Li-ion 800 mAhDurata della batteria: 220 h in standby / 5,5 h in conversazioneInclude: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolariDimensioni (senza base): 5 x 11 x 1,4 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellulare Thomson Serea51""> Maggiori Informazioni</a>",0.288,1,"Taxable Goods","Catalog, Search",85.9,,,,Telefono-Cellulare-Thomson-Serea51-Nero,"Telefono Cellulare Thomson Serea51 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","In arrivo il telefono cellulare Thomson Serea51 per gli amanti dei cellulari semplici e funzionali, per le persone anziane o per coloro che si avviciano per la prima volta al mondo della telefonia mobile!Caratteristiche:Cellulare sbloccatoSchermo: 1",http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400103_91208.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046186",33,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400103_91207.jpg,http://dropshipping.bigbuy.eu/imgs/V1400103_91206.jpg","GTIN=3527570046186",,,,,,,,,, +BB-V1400107,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Nero","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046094</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Nero,"Telefono Cellularel Thomson Serea62 Nero","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Nero,Nero,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046094",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046094",,,,,,,,,, +BB-V1400108,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Bianco","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046100</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Bianco,"Telefono Cellularel Thomson Serea62 Bianco","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Bianco,Bianco,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046100",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046100",,,,,,,,,, +BB-V1400109,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Telefonia e Accessori,Default Category/Informatica Elettronica/Telefonia e Accessori/Cellulari",base,"Telefono Cellularel Thomson Serea62 Rosso","<a id=""maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""><p>Se</a> sei alla ricerca di un <strong>cellulare</strong> semplice ma dal design impeccabile, non puoi lasciarti sfuggire il <strong>telefono cellulare Thomson Serea62</strong>. È studiato anche per le persone anziane che vogliono avvicinarsi al mondo della <strong>telefonia mobile</strong>. </p><p>Caratteristiche:</p><ul><li>Schermo: 2,4"" 240 x 320, 262 K a colori</li><li>Reti: GSM-EDGE 850/900/1800/1900 MHz</li><li>Tasto per le chiamate d'emergenza</li><li>Menù semplificato e facile da usare</li><li>Tasti di grandi dimensioni</li><li>Fotocamera VGA</li><li>Bluetooth</li><li>MP3</li><li>Radio FM</li><li>Luce LED</li><li>Micro USB</li><li>Micro SD (carta non inclusa)</li><li>Agenda: 250 voci</li><li>Funzione SMS, MMS</li><li>kit auricolare mani libere</li><li>Batteria Li-ion 800 mAh</li><li>Durata della batteria: 480 h in standby / 5,5 h in conversazione</li><li>Include: batteria, base di ricarica, adattatore di CA, cavo dati USB e auricolari</li><li>Dimensioni (senza base): 5,5 x 10,5 x 2 cm circa</li></ul><p> Dimenzioni per Telefono Cellularel Thomson Serea62: </br><ul><li>Altezza: 5.2 Cm</li><li>Larghezza: 11.3 Cm</li><li>Profondita': 18.6 Cm</li><li>Peso: 0.284 Kg</li></ul></p><p>Codice Prodotto (EAN): 3527570046117</p>","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62.</br><a href=""#maggiorni_informazioni"" title=""Telefono Cellularel Thomson Serea62""> Maggiori Informazioni</a>",0.284,1,"Taxable Goods","Catalog, Search",99.5,,,,Telefono-Cellularel-Thomson-Serea62-Rosso,"Telefono Cellularel Thomson Serea62 Rosso","Informatica Elettronica,Informatica,Elettronica,Telefonia e Accessori,Telefonia,Accessori,Cellulari,Colore Rosso,Rosso,","Se sei alla ricerca di un cellulare semplice ma dal design impeccabile, non puoi lasciarti sfuggire il telefono cellulare Thomson Serea62",http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,,http://dropshipping.bigbuy.eu/imgs/V1400106_91214.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=3527570046117",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1400106_91213.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91215.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91212.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91211.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91210.jpg,http://dropshipping.bigbuy.eu/imgs/V1400106_91209.jpg","GTIN=3527570046117",,,,,,,,,, +BB-V0100186,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Cuffie Fatina maginca Playz Kidz","<a id=""maggiorni_informazioni"" title=""Cuffie Fatina maginca Playz Kidz""><p>Le</a> <strong>cuffie Fatina Magica Playz Kidz </strong>son perfetti per i piccoli di casa! Queste <strong><strong>cuffie</strong> per bambini</strong> sono ideali come regalo per i re della casa!</p><p><a href=""http://www.playzkidz.com"" target=""_blank""><strong>www.playzkidz.com</strong></a></p><ul><li>Auricolari stereo</li><li>Cuffie imbottite</li><li>Compatibili con MP3, MP4, CD, radio e PC</li><li>Età raccomandata: +4 anni</li></ul><p> Dimenzioni per Cuffie Fatina maginca Playz Kidz: </br><ul><li>Altezza: 22.2 Cm</li><li>Larghezza: 9.5 Cm</li><li>Profondita': 26.7 Cm</li><li>Peso: 0.243 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888111122</p>","Le cuffie Fatina Magica Playz Kidz son perfetti per i piccoli di casa! Queste cuffie per bambini sono ideali come regalo per i re della casa!www.</br><a href=""#maggiorni_informazioni"" title=""Cuffie Fatina maginca Playz Kidz""> Maggiori Informazioni</a>",0.243,1,"Taxable Goods","Catalog, Search",18.9,,,,Cuffie-Fatina-maginca-Playz-Kidz,"Cuffie Fatina maginca Playz Kidz","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","Le cuffie Fatina Magica Playz Kidz son perfetti per i piccoli di casa! Queste cuffie per bambini sono ideali come regalo per i re della casa!www",http://dropshipping.bigbuy.eu/imgs/V0100186_93443.jpg,,http://dropshipping.bigbuy.eu/imgs/V0100186_93443.jpg,,,,,,"2016-08-16 05:37:23",,,,,,,,,,,,,,,,,"GTIN=4899888111122",2436,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0100186_93379.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93377.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93376.jpg,http://dropshipping.bigbuy.eu/imgs/V0100186_93375.jpg","GTIN=4899888111122",,,,,,,,,, +BB-V0100187,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Audio e Hi-Fi,Default Category/Informatica Elettronica/Audio e Hi-Fi/Cuffie",base,"Cuffie Mostriciattoli Playz Kidz","<a id=""maggiorni_informazioni"" title=""Cuffie Mostriciattoli Playz Kidz""><p>I</a> piccoli di casa impazziranno per le <strong><strong>cuffie</strong> Mostriciattoli Playz Kidz</strong>! Grazie al loro design originale e divertente, queste <strong><strong>cuffie</strong> per bambini </strong>sono il regalo perfetto!</p><p><a href=""http://www.playzkidz.com"" target=""_blank""><strong>www.playzkidz.com</strong></a></p><ul><li>Auricolari stereo</li><li>Cuffie imbottite</li><li>Compatibili con MP3, MP4, CD, radio e PC</li><li>Età raccomandata: +4 anni</li></ul><p> Dimenzioni per Cuffie Mostriciattoli Playz Kidz: </br><ul><li>Altezza: 22.2 Cm</li><li>Larghezza: 9.5 Cm</li><li>Profondita': 26.7 Cm</li><li>Peso: 0.245 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888111139</p>","I piccoli di casa impazziranno per le cuffie Mostriciattoli Playz Kidz! Grazie al loro design originale e divertente, queste cuffie per bambini sono il regalo perfetto!www.</br><a href=""#maggiorni_informazioni"" title=""Cuffie Mostriciattoli Playz Kidz""> Maggiori Informazioni</a>",0.245,1,"Taxable Goods","Catalog, Search",18.9,,,,Cuffie-Mostriciattoli-Playz-Kidz,"Cuffie Mostriciattoli Playz Kidz","Informatica Elettronica,Informatica,Elettronica,Audio e Hi-Fi,Audio,Hi-Fi,Cuffie,","I piccoli di casa impazziranno per le cuffie Mostriciattoli Playz Kidz! Grazie al loro design originale e divertente, queste cuffie per bambini sono il regalo perfetto!www",http://dropshipping.bigbuy.eu/imgs/V0100187_93370.jpg,,http://dropshipping.bigbuy.eu/imgs/V0100187_93370.jpg,,,,,,"2016-08-30 08:26:19",,,,,,,,,,,,,,,,,"GTIN=4899888111139",2438,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0100187_93374.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93373.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93372.jpg,http://dropshipping.bigbuy.eu/imgs/V0100187_93371.jpg","GTIN=4899888111139",,,,,,,,,, +BB-V1300174,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars R2-D2","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787128</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-R2-D2,"Orologio Sveglia con Contasecondi Star Wars R2-D2","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design R2-D2,R2-D2,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787128",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787128",,,,,,,,,, +BB-V1300175,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Stormtrooper","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787135</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Stormtrooper,"Orologio Sveglia con Contasecondi Star Wars Stormtrooper","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Stormtrooper,Stormtrooper,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787135",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787135",,,,,,,,,, +BB-V1300176,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Yoda","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787142</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Yoda,"Orologio Sveglia con Contasecondi Star Wars Yoda","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Yoda,Yoda,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787142",68,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787142",,,,,,,,,, +BB-V1300177,,Default,simple,"Default Category/Informatica Elettronica,Default Category/Informatica Elettronica/Orologi e Sveglie,Default Category/Informatica Elettronica/Orologi e Sveglie/Sveglie",base,"Orologio Sveglia con Contasecondi Star Wars Chewbacca","<a id=""maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""><p>Sorprendi</a> i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'<strong>o</strong><strong>rologio sveglia con contasecondi Star Wars</strong>! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</p><ul><li>Dispone di segnale acustico d'allarme e di pulsante per spegnerlo</li><li>Funziona a batterie (1 x AA, non inclusa)</li><li>Dimensioni: circa 10,5 x 13,5 x 6 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Orologio Sveglia con Contasecondi Star Wars: </br><ul><li>Altezza: 0.13 Cm</li><li>Larghezza: 0.1 Cm</li><li>Profondita': 0.5 Cm</li><li>Peso: 0.15 Kg</li></ul></p><p>Codice Prodotto (EAN): 8427934787159</p>","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi.</br><a href=""#maggiorni_informazioni"" title=""Orologio Sveglia con Contasecondi Star Wars""> Maggiori Informazioni</a>",0.15,1,"Taxable Goods","Catalog, Search",15.8,,,,Orologio-Sveglia-con-Contasecondi-Star-Wars-Chewbacca,"Orologio Sveglia con Contasecondi Star Wars Chewbacca","Informatica Elettronica,Informatica,Elettronica,Orologi e Sveglie,Orologi,Sveglie,Sveglie,Design Chewbacca,Chewbacca,","Sorprendi i tuoi familiari ed amici con un regalo originale che li lascerà a bocca aperta, l'orologio sveglia con contasecondi Star Wars! Una sveglia ideale per gli appassionati della famosa saga e dei suoi personaggi",http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,,http://dropshipping.bigbuy.eu/imgs/V1300173_102642.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8427934787159",69,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1300173_102640.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102644.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102643.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102641.jpg,http://dropshipping.bigbuy.eu/imgs/V1300173_102639.jpg","GTIN=8427934787159",,,,,,,,,, +BB-G1000110,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Offerte",base,"Attrezzo da Ginnastica Body Rocker","<a id=""maggiorni_informazioni"" title=""Attrezzo da Ginnastica Body Rocker ""><p>Non</a> perdere tempo e denaro andandoin <strong>palestra</strong> e procurarti ora l'<strong>attrezzo da ginnastica Body Rocker! </strong>Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa. Grazie al suo sistema di oscillazione, è perfetto per lavorare e rafforzare, spalle, braccia, schiena, petto, glutei, ecc. Fatto in acciaio con impugnature in gomma. Include manuale d'istruzioni e DVD dimostrativo. (Questo prodotto può può presentare lievi danni che non impediscono il funzionamento del prodotto: impugnature in gomma piuma scollate).</p><p> Dimenzioni per Attrezzo da Ginnastica Body Rocker : </br><ul><li>Altezza: 21 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 78 Cm</li><li>Peso: 2.13 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101314</p>","Non perdere tempo e denaro andandoin palestra e procurarti ora l'attrezzo da ginnastica Body Rocker! Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa.</br><a href=""#maggiorni_informazioni"" title=""Attrezzo da Ginnastica Body Rocker ""> Maggiori Informazioni</a>",2.13,1,"Taxable Goods","Catalog, Search",79,,,,Attrezzo-da-Ginnastica-Body-Rocker,"Attrezzo da Ginnastica Body Rocker","Outlet Offerte,Outlet,Offerte,Offerte,","Non perdere tempo e denaro andandoin palestra e procurarti ora l'attrezzo da ginnastica Body Rocker! Una forma facile ed efficace di rimettresi in forma facendo esercizio in casa",http://dropshipping.bigbuy.eu/imgs/bodirocker-00.jpg,,http://dropshipping.bigbuy.eu/imgs/bodirocker-00.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=4899888101314",121,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/bodirocker-03.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-06.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-05.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-07.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-02.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-01.jpg,http://dropshipping.bigbuy.eu/imgs/bodirocker-04.jpg","GTIN=4899888101314",,,,,,,,,, +BB-J2000066,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Arancio","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Lovely Arancio,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Arancio","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Lovely Arancio,Lovely Arancio,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000239,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Commando","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Commando,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Commando","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Commando,Commando,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000240,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Galaktic","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Galaktic,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Galaktic","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Galaktic,Galaktic,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,,,,,,"2016-02-11 15:49:04",,,,,,,,,,,,,,,,,"GTIN=4899888102731",14,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000329,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Blu","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""><p><strong>Acquista</a> la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali</strong>.<strong> </strong>La <strong>coperta Snug Snug con le maniche</strong> ti riscalderà senza aver bisogno del riscaldamento per tutto il giorno. Risparmierai i soldi e starai comodo grazie alla <strong>coperta extra morbida Kangoo Snug Snug con le maniche</strong>, poiché potrai fare tutto ciò che vuoi mentre sei coperto.</p><p>La coperta Snug Snug con le maniche è il regalo perfetto per qualsiasi occasione. Nessuno si aspetta un regalo così straordinario! Questa coperta con le maniche è l'ideale per quei momenti in cui fa freddo fuori e vuoi solo stare a casa a leggere, usare il computer o semplicemente a rilassarti sul divano. Fino ad ora, non è era facile raggomitolarsi sul divano e voler fare qualcosa, perché dovevi tirare fuori le braccia, ma faceva freddo. Tutto questo è finito grazie alla coperta Kangoo Snug Snug con le maniche!</p><p>Caratteristiche:</p><ul><li>Fatto di pile ultra soffice</li><li>Lavabile in lavatrice</li><li>Dimensioni: lunghezza 185 cm, larghezza 160 cm | Dimensioni manica: 60cm</li></ul><p><a title=""Batamanta SnugSnug"" href=""http://www.snugsnug.com"" target=""_blank"">www.snugsnug.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio): </br><ul><li>Altezza: 28 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 12 Cm</li><li>Peso: 0.8 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102731</p>","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio)""> Maggiori Informazioni</a>",0.8,1,"Taxable Goods","Catalog, Search",22.9,,,,OUTLET-Coperta-super-soffice-Kangoo-Snug-Snug-con-maniche-per-adulti-|-Decorazioni-originali-(Senza-imballaggio)-Lovely Blu,"OUTLET Coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali (Senza imballaggio) Lovely Blu","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Lovely Blu,Lovely Blu,","Acquista la coperta super soffice Kangoo Snug Snug con maniche per adulti | Decorazioni originali",http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000065_91717.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888102731",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000065_91715.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91719.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91718.jpg,http://dropshipping.bigbuy.eu/imgs/J2000065_91716.jpg","GTIN=4899888102731",,,,,,,,,, +BB-J2000085,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Rosa S","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Rosa-S,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Rosa S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Rosa,Rosa,Taglia S,S,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000086,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola M","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Viola-M,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola M","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Viola,Viola,Taglia M,M,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000188,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola L","<a id=""maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""><p>Acquista</a> <strong>Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</strong> Scopri il modo più facile per mantenerti caldo a casa quando c'è freddo! Devi solo riscaldare questi <strong>stivali rilassanti nel microonde</strong> per godere immediatamente del calore e del rilassamento ai tuoi piedi. Sono fatti di morbido tessuto polare e di un rivestimento in semi di lavanda che emana un <strong>profumo straordinario</strong> quando riscaldato. Goditi semplicemente il profumo rilassante! I Warm Hug Feet Stivali Riscaldabili al Microonde mantengono il calore per lungo tempo grazie ai semi di lavanda contenuti all'interno. Per pulire questi stivali riscaldabili, passaci sopra un panno umido. Non riscaldarmi mai per più di 2 minuti e non usarli se sono troppo caldi. Disponibili in rosa e blu.</p><p>Taglie (equivalenze approssimative)</p><ul><li>S (35 a 38)</li><li>M (38 a 41)</li><li>L (41 a 43)</li></ul><p>Rispettare sempre i limiti nel microonde</p><ul><li>800W</li><li>80ºC</li><li>990 sec</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio): </br><ul><li>Altezza: 27 Cm</li><li>Larghezza: 10.5 Cm</li><li>Profondita': 29 Cm</li><li>Peso: 0.67 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899881028786</p>","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio)""> Maggiori Informazioni</a>",0.67,1,"Taxable Goods","Catalog, Search",33.5,,,,OUTLET-Warm-Hug-Feet-Stivali-Riscaldabili-al-Microonde-(Senza-imballaggio)-Viola-L,"OUTLET Warm Hug Feet Stivali Riscaldabili al Microonde (Senza imballaggio) Viola L","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Viola,Viola,Taglia L,L,","Acquista Warm Hug Feet Stivali Riscaldabili al Microonde al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-01.jpg,,,,,,"2016-02-02 10:45:18",,,,,,,,,,,,,,,,,"GTIN=4899881028786",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-00.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-03.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-02.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-04.jpg,http://dropshipping.bigbuy.eu/imgs/warm-hugh-feet-boots-05.jpg","GTIN=4899881028786",,,,,,,,,, +BB-J2000123,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)""><p>Avviso</a> per gli amanti del gelato: ecco la nuovissima <strong>macchina per gelato</strong> <strong>Princess 282602</strong>. Una <strong>gelatiera</strong> diversa dalle altre, che ti permetterà di preparare in un batter d'occhio gelati dolci o salati per i grandi e i più piccini!<strong> </strong>Al naturale o con salsa di frutta, pepite o qualsiasi altra guarnizione...le tue papille ti ringrazieranno! Indispensabile per un'estate al fresco!</p><p>Basta mettere in freezer, per 12 ore circa, il recipiente rimovibile a forma di secchiello con manico integrato di 17,5 x 14,5 cm (diametro x altezza) ed è fatto! Il gelato 100% naturale è servito!</p><p>Caratteristiche:</p><ul><li>Potenza: 5 W</li><li>Frequenza: 50 Hz</li><li>Tensione: 220-240 V</li><li>Pulsante On/Off </li><li>Gommini antiscivolo</li><li>Scomparto per cavo di alimentazione</li><li>Mescolatore rimovibile</li><li>Facile da pulire</li><li>Apertura di riempimento (dimensioni approssimative: 5 x 5,5 cm)</li><li>Dimensioni approssimative: 22 x 25 x 22 cm</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio): </br><ul><li>Altezza: 31 Cm</li><li>Larghezza: 23.4 Cm</li><li>Profondita': 23.5 Cm</li><li>Peso: 3.26 Kg</li></ul></p><p>Codice Prodotto (EAN): 8712836304895</p>","Avviso per gli amanti del gelato: ecco la nuovissima macchina per gelato Princess 282602.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)""> Maggiori Informazioni</a>",3.26,1,"Taxable Goods","Catalog, Search",110.88,,,,OUTLET-Macchina-per-Gelato-Princess-282602-(Senza-imballaggio),"OUTLET Macchina per Gelato Princess 282602 (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Avviso per gli amanti del gelato: ecco la nuovissima macchina per gelato Princess 282602",http://dropshipping.bigbuy.eu/imgs/J2000123_88610.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000123_88610.jpg,,,,,,"2016-08-09 01:54:36",,,,,,,,,,,,,,,,,"GTIN=8712836304895",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000123_88612.jpg,http://dropshipping.bigbuy.eu/imgs/J2000123_88611.jpg","GTIN=8712836304895",,,,,,,,,, +BB-J2000176,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Celeste","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Celeste,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Celeste","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Celeste,Celeste,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",5,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000178,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Rosso","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Rosso,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Rosso","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Rosso,Rosso,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",3,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000179,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cioccolato","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Cioccolato,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cioccolato","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Cioccolato,Cioccolato,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",5,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000180,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Crudo","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Crudo,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Crudo","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Crudo,Crudo,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",9,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000181,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Fragola","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Fragola,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Fragola","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Fragola,Fragola,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000182,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cardinale","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Cardinale,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Cardinale","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Cardinale,Cardinale,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000247,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Turchese","<a id=""maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""><p><strong>Acquistare</a> Coperta Eden Deluxe </strong>160 x 240 al miglior prezzo<strong>. </strong>Questa fantastica <strong>coperta Eden Deluxe </strong>è perfetta per il tuo letto. La <strong>coperta Eden Dexuxe </strong>misura 160 x 240 cm. Questa coperta è veramente soffice e confortevole. La coperta Eden Deluxe è l'ideale per godere di un caldo inverno. 100% poliestere. Con comoda maniglia da trasporto.</p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio): </br><ul><li>Altezza: 43 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 51.5 Cm</li><li>Peso: 2.183 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436045510426</p>","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio)""> Maggiori Informazioni</a>",2.183,1,"Taxable Goods","Catalog, Search",57.9,,,,OUTLET-Coperta-Eden-Deluxe-160-x-240-(Senza-imballaggio)-Turchese,"OUTLET Coperta Eden Deluxe 160 x 240 (Senza imballaggio) Turchese","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Design Turchese,Turchese,","Acquistare Coperta Eden Deluxe 160 x 240 al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000175_88982.jpg,,,,,,"2016-02-17 15:45:14",,,,,,,,,,,,,,,,,"GTIN=8436045510426",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000175_88981.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88987.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88986.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88985.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88984.jpg,http://dropshipping.bigbuy.eu/imgs/J2000175_88983.jpg","GTIN=8436045510426",,,,,,,,,, +BB-J2000177,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)""><p>Hai</a> visto la <strong>macchina RC Ferrari 599 GTO</strong>? Puoi facilmente usare questo divertente giocattolo<strong> autorizzato Ferrari</strong> grazie al suo telecomando. La macchina funziona a batterie 5 x AA (non incluse) e il telecomando a batteria 1 x 6F22 9V (non inclusa). Giocattolo adatto per bambini di età superiore ai 6 anni. Funzioni della macchina radiocontrollata Ferrari:</p><ul><li>Avanti e indietro</li><li>Gira a sinistra e a destra</li><li>Fari e Fanali posteriori</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio): </br><ul><li>Altezza: 17.5 Cm</li><li>Larghezza: 43.5 Cm</li><li>Profondita': 22.7 Cm</li><li>Peso: 1.244 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158011299</p>","Hai visto la macchina RC Ferrari 599 GTO? Puoi facilmente usare questo divertente giocattolo autorizzato Ferrari grazie al suo telecomando.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)""> Maggiori Informazioni</a>",1.244,1,"Taxable Goods","Catalog, Search",119,,,,OUTLET-Macchina-RC-Ferrari-599-GTO--(Senza-imballaggio),"OUTLET Macchina RC Ferrari 599 GTO (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Hai visto la macchina RC Ferrari 599 GTO? Puoi facilmente usare questo divertente giocattolo autorizzato Ferrari grazie al suo telecomando",http://dropshipping.bigbuy.eu/imgs/J2000177_89020.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000177_89020.jpg,,,,,,"2016-07-22 06:19:54",,,,,,,,,,,,,,,,,"GTIN=8718158011299",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000177_89022.jpg,http://dropshipping.bigbuy.eu/imgs/J2000177_89021.jpg","GTIN=8718158011299",,,,,,,,,, +BB-J2000255,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige S","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-S,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia S,S,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,,,,,,"2016-02-12 08:36:52",,,,,,,,,,,,,,,,,"GTIN=4899888101352",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000285,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige L","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-L,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige L","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia L,L,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101352",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000279,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige M","<a id=""maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""><p>Metti</a> in luce un corpo scultoreo con la <strong>Canottiera Modellante con Reggiseno Booby & Tummy! </strong>Discreta, comod. Sostiene il seno e offre una grande capacità di sostegno. Prodotta il un tessuto delicato, flessibile e traspirante che si adatta perfettamente al tuo corpo e offre una copertura completa (seno, fianchi e glutei).  Equivalenza di taglie: S: 36-38, M: 38-40, L: 40-42.</p><p><a href=""http://www.boobyandtummy.com/"" target=""_blank""><strong>www.boobyandtummy.com</strong></a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 13.5 Cm</li><li>Profondita': 7 Cm</li><li>Peso: 0.14 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101352</p>","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio)""> Maggiori Informazioni</a>",0.14,1,"Taxable Goods","Catalog, Search",23.1,,,,OUTLET-Canottiera-Modellante-con-Reggiseno-Booby-&-Tummy--(Senza-imballaggio)-Beige-M,"OUTLET Canottiera Modellante con Reggiseno Booby & Tummy (Senza imballaggio) Beige M","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Colore Beige,Beige,Taglia M,M,","Metti in luce un corpo scultoreo con la Canottiera Modellante con Reggiseno Booby & Tummy! Discreta, comod",http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000254_89905.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101352",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000254_89903.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89906.jpg,http://dropshipping.bigbuy.eu/imgs/J2000254_89904.jpg","GTIN=4899888101352",,,,,,,,,, +BB-J2000264,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)""><p>La <strong>scopa</a> elettrica triangolare 360 Sweep</strong><strong> </strong>è perfetta per pulire in modo facile, rapido ed efficace. Grazie alla sua tecnologia innovativa, le setole ruotano automaticamente per rimuovere a fondo tutto lo sporco. Inoltre, questa <strong>scopa elettrica</strong> è leggera e facile da usare, il che la rende molto comoda e pratica. La scopa elettrica Sweep ruota di 360º e raggiunge facilmente qualsiasi angolo di casa.</p><p>Prova la nuova scopa elettrica triangolare 360 Sweep e scopri un modo migliore per pulire!</p><p>Caratteristiche della Scopa elettrica triangolare 360 Sweep:</p><ul><li>Scopa elettrica senza fili</li><li>Setole rotanti</li><li>Bastone in alluminio removibile (c.ca 115cm)</li><li>Base piatta triangolare (3 lati identici: circa 33cm di lunghezza x 3cm di altezza)</li><li>Batteria ricaricabile  7.2V</li><li>Caricabatteria (230V, 50Hz)</li><li>Durata batteria: Approx. 30 min</li><li>Scopartimento interno raccogli-polvere</li><li>Leggera, facile da usare, svuotare e pulire</li><li>Estremamente silenziosa</li></ul><p> <a title=""Escoba Eléctrica Sweep 360"" href=""http://www.360sweep.com/"" target=""_blank"">www.360sweep.com</a></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio): </br><ul><li>Altezza: 32 Cm</li><li>Larghezza: 39.5 Cm</li><li>Profondita': 9.5 Cm</li><li>Peso: 1.625 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888102458</p>","La scopa elettrica triangolare 360 Sweep è perfetta per pulire in modo facile, rapido ed efficace.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)""> Maggiori Informazioni</a>",1.625,1,"Taxable Goods","Catalog, Search",89.9,,,,OUTLET-Scopa-elettrica-triangolare-senza-fili-360-Sweep-(Senza-imballaggio),"OUTLET Scopa elettrica triangolare senza fili 360 Sweep (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","La scopa elettrica triangolare 360 Sweep è perfetta per pulire in modo facile, rapido ed efficace",http://dropshipping.bigbuy.eu/imgs/J2000264_89527.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000264_89527.jpg,,,,,,"2016-09-01 06:15:11",,,,,,,,,,,,,,,,,"GTIN=4899888102458",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000264_89533.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89532.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89531.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89530.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89529.jpg,http://dropshipping.bigbuy.eu/imgs/J2000264_89528.jpg","GTIN=4899888102458",,,,,,,,,, +BB-J2000291,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)","<a id=""maggiorni_informazioni"" title=""OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)""><p>Porta</a> la spiaggia a casa con la <strong>sabbia kinetic per bambini Playz Kidz</strong>! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia. Questo divertente ed originale gioco sviluppa i talenti artistici e la creatività dei bambini. Comprende circa 0,5 kg di sabbia kinetic e 3 accessori in plastica: un rullo, una forcella e una spatola. Non tossico. Non macchia i vestiti o si attacca alle mani. Età consigliata: 3+ anni</p><p><strong><a href=""http://www.playzkidz.com"">www.playzkidz.com</a></strong></p><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio): </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 14 Cm</li><li>Profondita': 14 Cm</li><li>Peso: 0.65 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888108085</p>","Porta la spiaggia a casa con la sabbia kinetic per bambini Playz Kidz! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)""> Maggiori Informazioni</a>",0.65,1,"Taxable Goods","Catalog, Search",11.9,,,,OUTLET-Sabbia-Kinetic-per-Bambini-Playz-Kidz--(Senza-imballaggio),"OUTLET Sabbia Kinetic per Bambini Playz Kidz (Senza imballaggio)","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,","Porta la spiaggia a casa con la sabbia kinetic per bambini Playz Kidz! Il regalo perfetto per sorprendere i tuoi piccoli, che si divertiranno a costruire ogni tipo di castello e forma di sabbia",http://dropshipping.bigbuy.eu/imgs/J2000291_90163.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000291_90163.jpg,,,,,,"2016-08-09 01:48:11",,,,,,,,,,,,,,,,,"GTIN=4899888108085",778,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000291_90165.jpg,http://dropshipping.bigbuy.eu/imgs/J2000291_90164.jpg","GTIN=4899888108085",,,,,,,,,, +BB-J2000350,,Default,simple,"Default Category/Outlet Offerte,Default Category/Outlet Offerte/Senza imballaggio",base,"OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio) S","<a id=""maggiorni_informazioni"" title=""OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio)""><p><strong>Acquista</a> il Reggiseno Crochet </strong>(3 pezzi)<strong> al miglior prezzo. </strong>Sentiti fantastica e sexy per tutto il giorno! Dimentica i fili, fascette o spalline. Il Crochet Bra utilizza la tecnologia <em>Woven Everlast</em> per un comfort massimo. Questi reggiseni sono stati elaborati senza tutti quegli elementi in modo da essere usato comodamente dimenticando che lo stai indossando. Questo reggiseno si adatta al tuo seno, sollevandolo e sostenendolo. Il Crochet Bra si adatta perfettamente al tuo corpo e forma , senza lasciare segni o pieghe. In più è morbido, flessibile e molto comodo, e si adatta ad ogni coppa, sollevando il tio seno in modo significativo.</p><p><a href=""http://www.crochetbra.com"" target=""_blank"">www.crochetbra.com</a><br /><strong><br />Caratteristiche:</strong></p><ul><li>Lavabile in lavatrice</li><li>Bretelle comode</li><li>Fatto al 96% di nylon e 4% di spandex</li><li>Adattabile alla forma del tuo seno</li><li>Solleva il tuo seno</li><li>La confezione include 3 reggiseni (1 beige, 1 nero and 1 bianco)</li><li>Equivalenza taglie appross.: S: 80/85 - M: 90/95 - L: 100/105</li></ul><p><strong>NOTA: Questo prodotto presenta lievi danni estetici, quali graffi o sfregature, che non influiscono sul suo funzionamento. Spedito in confezione standard.</strong></p><p> Dimenzioni per OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio): </br><ul><li>Altezza: 4.5 Cm</li><li>Larghezza: 15.5 Cm</li><li>Profondita': 27 Cm</li><li>Peso: 0.269 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101345</p>","Acquista il Reggiseno Crochet (3 pezzi) al miglior prezzo.</br><a href=""#maggiorni_informazioni"" title=""OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio)""> Maggiori Informazioni</a>",0.269,1,"Taxable Goods","Catalog, Search",34.9,,,,OUTLET-Reggiseno-Crochet-Bra-(3-Pezzi)-(Senza-imballaggio)-S,"OUTLET Reggiseno Crochet Bra (3 Pezzi) (Senza imballaggio) S","Outlet Offerte,Outlet,Offerte,Senza imballaggio,Senza,imballaggio,Taglia S,S,","Acquista il Reggiseno Crochet (3 pezzi) al miglior prezzo",http://dropshipping.bigbuy.eu/imgs/J2000349_93435.jpg,,http://dropshipping.bigbuy.eu/imgs/J2000349_93435.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=4899888101345",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/J2000349_93438.jpg,http://dropshipping.bigbuy.eu/imgs/J2000349_93437.jpg,http://dropshipping.bigbuy.eu/imgs/J2000349_93436.jpg","GTIN=4899888101345",,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv b/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv new file mode 100644 index 0000000000000..0ea052a043526 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/BB-ProductsWorking.csv @@ -0,0 +1,29 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +BB-D2010129,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Nero","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888101772</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Nero,"Ventilatore Portatile Spray FunFan Nero","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Nero,Nero,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888101772",41,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888101772",,,,,,,,,, +BB-D2010130,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Bianco","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107965</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Bianco,"Ventilatore Portatile Spray FunFan Bianco","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Bianco,Bianco,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107965",741,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107965",,,,,,,,,, +BB-D2010131,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Sistemi di Climatizzazione,Default Category/Casa Giardino/Sistemi di Climatizzazione/Aria condizionata e ventilatori",base,"Ventilatore Portatile Spray FunFan Rosso","<a id=""maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""><p>Se</a> sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il <strong>ventilatore portatile spray FunFan</strong>. Si tratta di una soluzione pratica per mantenersi al fresco in una moltitudine di situazioni, come escursioni, gite in spiaggia, mentre si fa sport, ecc. Inoltre, grazie alle sue dimensioni ridotte (dimensioni: circa 9 x 26 x 6,5 cm) e peso ridotto (circa 130 g), lo puoi portare ovunque!<br /><br /><a href=""http://www.myfunfan.com""><strong>www.myfunfan.com</strong></a><br /><br />Questo <strong>ventilatore portatile</strong> originale ha un pulsante per attivare le eliche in PVC malleabili e una leva che spruzza l'acqua. Cosa c'è di più, puoi aggiungere il ghiaccio per aumentare la sensazione di freddo! Include 1 cacciavite a croce per inserire le batterie. Realizzato in PVC. Funzionamento a batterie (2 x AA, non incluse).</p><p> Dimenzioni per Ventilatore Portatile Spray FunFan : </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 28 Cm</li><li>Profondita': 7.5 Cm</li><li>Peso: 0.185 Kg</li></ul></p><p>Codice Prodotto (EAN): 4899888107972</p>","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore Portatile Spray FunFan ""> Maggiori Informazioni</a>",0.185,1,"Taxable Goods","Catalog, Search",19.9,,,,Ventilatore-Portatile-Spray-FunFan-Rosso,"Ventilatore Portatile Spray FunFan Rosso","Casa Giardino,Casa,Giardino,Sistemi di Climatizzazione,Sistemi,Climatizzazione,Aria condizionata e ventilatori,Aria,condizionata,ventilatori,Colore Rosso,Rosso,","Se sei il tipo di persona che è sempre alla ricerca di prodotti innovativi per combattere il caldo estivo, non perderti il ventilatore portatile spray FunFan",http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,http://dropshipping.bigbuy.eu/imgs/D2010128_78890.jpg,,,,,,"2016-01-12 09:56:53",,,,,,,,,,,,,,,,,"GTIN=4899888107972",570,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/D2010128_78887.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78898.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78889.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78888.jpg,http://dropshipping.bigbuy.eu/imgs/D2010128_78886.jpg","GTIN=4899888107972",,,,,,,,,, +BB-H1000163,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005922</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0592 Blu Marino,"Sedia Pieghevole Campart Travel CH0592 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0592 Blu Marino,CH0592 Blu Marino,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005922",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005922",,,,,,,,,, +BB-H1000162,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Sedia Pieghevole Campart Travel CH0596 Grigio","<a id=""maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""><p>Se</a> stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la <strong>sedia pieghevole </strong><strong>Campart Travel</strong>! Questa <strong>sedia da campeggio imbottita</strong> è perfetta per i luoghi di campeggio, cortili, giardini, ecc. Ideale per il riposo e il relax. Può portare fino a 120 kg. Dimensioni: 66 x 70 / 120 x 87 / 115 cm circa. Semplice da trasportare ovunque, grazie al suo design funzionale ed elegante (dimensioni quando piegato: circa 66 x 110 x 10 cm). 7 posizioni regolabili e un poggiatesta incorporato. Struttura in alluminio e stoffa imbottita in poliestere. Altezza sedia: circa 50 cm.</p><p> Dimenzioni per Sedia Pieghevole Campart Travel: </br><ul><li>Altezza: 10 Cm</li><li>Larghezza: 66 Cm</li><li>Profondita': 110 Cm</li><li>Peso: 5.3 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005960</p>","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc.</br><a href=""#maggiorni_informazioni"" title=""Sedia Pieghevole Campart Travel""> Maggiori Informazioni</a>",5.3,1,"Taxable Goods","Catalog, Search",129,,,,Sedia-Pieghevole-Campart-Travel-CH0596 Grigio,"Sedia Pieghevole Campart Travel CH0596 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0596 Grigio,CH0596 Grigio,","Se stai cercando comfort e convenienza allo stesso tempo, probabilmente adorerai la sedia pieghevole Campart Travel! Questa sedia da campeggio imbottita è perfetta per i luoghi di campeggio, cortili, giardini, ecc",http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_02.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005960",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_00.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_04.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_03.jpg,http://dropshipping.bigbuy.eu/imgs/silla_plegable_camping_01.jpg","GTIN=8713016005960",,,,,,,,,, +BB-F1520329,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005939</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0593 Blu Marino,"Poggiapiedi Pieghevole Campart Travel CH0593 Blu Marino","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0593 Blu Marino,CH0593 Blu Marino,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005939",1,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005939",,,,,,,,,, +BB-F1520328,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Decorazione, illuminazione e mobili",base,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","<a id=""maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""><p>Approfitta</a> di un'esperienza rilassante con l'aiuto del <strong>poggiapiedi pieghevole Campart Travel</strong>! Questo<strong> poggiapiedi imbottito</strong> è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc. Possiede 2 ganci di circa 3 cm di diametro che possono essere facilmente attaccate alla barra inferiore delle sedie (utilizzabile solo per sedie con una barra inferiore di circa 2 cm di diametro). Struttura in alluminio. Tessuto: poliestere. Dimensioni: circa 51 x 47 x 96 cm (dimensioni quando ripiegato: circa 51 x 12 x 96 cm). Ideale per le sedie pieghevoli Campart Travel CH0592 e CH0596.</p><p> Dimenzioni per Poggiapiedi Pieghevole Campart Travel: </br><ul><li>Altezza: 13 Cm</li><li>Larghezza: 53 Cm</li><li>Profondita': 97 Cm</li><li>Peso: 1.377 Kg</li></ul></p><p>Codice Prodotto (EAN): 8713016005977</p>","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc.</br><a href=""#maggiorni_informazioni"" title=""Poggiapiedi Pieghevole Campart Travel""> Maggiori Informazioni</a>",1.377,1,"Taxable Goods","Catalog, Search",46.6,,,,Poggiapiedi-Pieghevole-Campart-Travel-CH0597 Grigio,"Poggiapiedi Pieghevole Campart Travel CH0597 Grigio","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Decorazione, illuminazione e mobili,Decorazione,,illuminazione,mobili,Referenza e Colore CH0597 Grigio,CH0597 Grigio,","Approfitta di un'esperienza rilassante con l'aiuto del poggiapiedi pieghevole Campart Travel! Questo poggiapiedi imbottito è l'accessorio ideale per sedie da cortile, terrazzo, giardini, luoghi da campeggio, ecc",http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_01.jpg,,,,,,"2015-09-21 15:58:54",,,,,,,,,,,,,,,,,"GTIN=8713016005977",7,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_00.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_02.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_08.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_07.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_06.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_05.jpg,http://dropshipping.bigbuy.eu/imgs/reposapies_CH-0609_04.jpg","GTIN=8713016005977",,,,,,,,,, +BB-H4502058,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Star Wars","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Star Wars""><p>I</a> fan di Star Wars non potranno fare a meno di appendere l'<strong>orologio da parete Star Wars</strong> in casa loro! Realizzato in plastica. Funziona a batterie (1 x AA, non incluse). Diametro circa: 25,5 cm. Spessore circa: 3,5 cm.</p><p> Dimenzioni per Orologio da Parete Star Wars: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 26 Cm</li><li>Profondita': 3.8 Cm</li><li>Peso: 0.287 Kg</li></ul></p><p>Codice Prodotto (EAN): 6950687214204</p>","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Star Wars""> Maggiori Informazioni</a>",0.287,1,"Taxable Goods","Catalog, Search",22.5,,,,Orologio-da-Parete-Star-Wars,"Orologio da Parete Star Wars","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","I fan di Star Wars non potranno fare a meno di appendere l'orologio da parete Star Wars in casa loro! Realizzato in plastica",http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,http://dropshipping.bigbuy.eu/imgs/H4502058_84712.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=6950687214204",130,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/H4502058_84713.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84711.jpg,http://dropshipping.bigbuy.eu/imgs/H4502058_84710.jpg","GTIN=6950687214204",,,,,,,,,, +BB-G0500195,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Rosso","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Rosso,"Braccialetto Sportivo a LED MegaLed Rosso","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Rosso,Rosso,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-G0500196,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Illuminazione LED",base,"Braccialetto Sportivo a LED MegaLed Verde","<a id=""maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""><p>Se</a> ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed. Con questo<strong> braccialetto di sicurezza</strong> sarai visibile ai motorini e alle auto nell'oscurità, così da poter stare molto più sicuro e tranquillo. È dotato di 2 luci a LED con 2 possibili soluzioni (luce fissa ed intermittente). La lunghezza massima è di circa 55 cm e quella minima è di circa 42 cm. Molto leggero (circa 50 g). Autonomia circa: 24-40 ore. Funziona a batterie (2 x CR2023, incluse).</p><p> </p><p> Dimenzioni per Braccialetto Sportivo a LED MegaLed: </br><ul><li>Altezza: 3 Cm</li><li>Larghezza: 20 Cm</li><li>Profondita': 4 Cm</li><li>Peso: 0.049 Kg</li></ul></p><p>Codice Prodotto (EAN): 8436545443507</p>","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed.</br><a href=""#maggiorni_informazioni"" title=""Braccialetto Sportivo a LED MegaLed""> Maggiori Informazioni</a>",0.049,1,"Taxable Goods","Catalog, Search",22,,,,Braccialetto-Sportivo-a-LED-MegaLed-Verde,"Braccialetto Sportivo a LED MegaLed Verde","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Illuminazione LED,Illuminazione,LED,Colore Verde,Verde,","Se ti piace praticare la corsa, il ciclismo, o qualunque sport all'aria aperta, non può mancare tra i tuoi accessori il braccialetto per lo sport a LED MegaLed",http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,http://dropshipping.bigbuy.eu/imgs/G0500194_87775.jpg,,,,,,"2016-01-08 12:34:41",,,,,,,,,,,,,,,,,"GTIN=8436545443507",37,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/G0500194_87774.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87773.jpg,http://dropshipping.bigbuy.eu/imgs/G0500194_87772.jpg","GTIN=8436545443507",,,,,,,,,, +BB-I2500333,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Mom's Diner","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""><p>Decora</a> la tua cucina con l'originale <strong>orologio da parete</strong> <strong>Mom's Diner</strong> in stile vintage! È realizzato in legno. Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Mom's Diner: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345052</p>","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Mom's Diner""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Mom's-Diner,"Orologio da Parete Mom's Diner","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Decora la tua cucina con l'originale orologio da parete Mom's Diner in stile vintage! È realizzato in legno",http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500333_88061.jpg,,,,,,"2016-07-21 13:05:12",,,,,,,,,,,,,,,,,"GTIN=4029811345052",2,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500333_88060.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88059.jpg,http://dropshipping.bigbuy.eu/imgs/I2500333_88058.jpg","GTIN=4029811345052",,,,,,,,,, +BB-I2500334,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Orologi da parete e da tavolo",base,"Orologio da Parete Coffee Endless Cup","<a id=""maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""><p>Se</a> sei un appassionato di caffè, non puoi rimanere senza l'<strong>orologio da parete Coffee Endless Cup</strong>! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa. Spessore: 0,8 cm circa. Funziona a pile (1 x AA, non inclusa).</p><p> Dimenzioni per Orologio da Parete Coffee Endless Cup: </br><ul><li>Altezza: 59 Cm</li><li>Larghezza: 59 Cm</li><li>Profondita': 6 Cm</li><li>Peso: 2.1 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811345069</p>","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa.</br><a href=""#maggiorni_informazioni"" title=""Orologio da Parete Coffee Endless Cup""> Maggiori Informazioni</a>",2.1,1,"Taxable Goods","Catalog, Search",42.5,,,,Orologio-da-Parete-Coffee-Endless-Cup,"Orologio da Parete Coffee Endless Cup","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Orologi da parete e da tavolo,Orologi,parete,tavolo,","Se sei un appassionato di caffè, non puoi rimanere senza l'orologio da parete Coffee Endless Cup! Un orologio vintage in legno con un design in perfetto stile caffettoso! Diametro: 58 cm circa",http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,http://dropshipping.bigbuy.eu/imgs/I2500334_88065.jpg,,,,,,"2016-08-30 13:41:52",,,,,,,,,,,,,,,,,"GTIN=4029811345069",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/I2500334_88064.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88063.jpg,http://dropshipping.bigbuy.eu/imgs/I2500334_88062.jpg","GTIN=4029811345069",,,,,,,,,, +BB-V0000252,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Stop!","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346196</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Stop!,"Insegna Dito Vintage Look Stop!","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Stop!,Stop!,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346196",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346196",,,,,,,,,, +BB-V0000253,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Adults Only","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346202</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Adults Only,"Insegna Dito Vintage Look Adults Only","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Adults Only,Adults Only,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346202",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346202",,,,,,,,,, +BB-V0000254,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Insegna Dito Vintage Look Talk","<a id=""maggiorni_informazioni"" title=""Insegna Dito Vintage Look""><p>Se</a> sei alla ricerca di una <strong>decorazione vintage</strong> originale e divertente per la tua casa, l'<strong>insegna dito Vintage Look</strong> ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno. Dimensioni: 69 x 17 x 1 cm circa.</p><p> Dimenzioni per Insegna Dito Vintage Look: </br><ul><li>Altezza: 17 Cm</li><li>Larghezza: 69 Cm</li><li>Profondita': 1 Cm</li><li>Peso: 0.63 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346318</p>","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno.</br><a href=""#maggiorni_informazioni"" title=""Insegna Dito Vintage Look""> Maggiori Informazioni</a>",0.63,1,"Taxable Goods","Catalog, Search",15.99,,,,Insegna-Dito-Vintage-Look-Talk,"Insegna Dito Vintage Look Talk","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Talk,Talk,","Se sei alla ricerca di una decorazione vintage originale e divertente per la tua casa, l'insegna dito Vintage Look ti conquisterà con il suo stile e i suoi simpatici messaggi! Realizzata in legno",http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000251_89744.jpg,,,,,,"2016-02-29 09:49:10",,,,,,,,,,,,,,,,,"GTIN=4029811346318",8,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000251_89745.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89746.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89743.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89742.jpg,http://dropshipping.bigbuy.eu/imgs/V0000251_89741.jpg","GTIN=4029811346318",,,,,,,,,, +BB-V0000256,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Go Left","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346325</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Go Left,"Freccia Decorativa Vintage Look Go Left","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Go Left,Go Left,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346325",4,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346325",,,,,,,,,, +BB-V0000257,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Exit","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346332</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Exit,"Freccia Decorativa Vintage Look Exit","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Exit,Exit,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346332",19,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346332",,,,,,,,,, +BB-V0000258,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Altri articoli decorativi",base,"Freccia Decorativa Vintage Look Cold beer here","<a id=""maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""><p>Stupisci</a> tutti con la divertente ed originale <strong>freccia decorativa Vintage Look</strong>! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno. Misure appross.: 25 x 40 x 1 cm.</p><p> Dimenzioni per Freccia Decorativa Vintage Look: </br><ul><li>Altezza: 25.5 Cm</li><li>Larghezza: 0.8 Cm</li><li>Profondita': 40 Cm</li><li>Peso: 0.376 Kg</li></ul></p><p>Codice Prodotto (EAN): 4029811346349</p>","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno.</br><a href=""#maggiorni_informazioni"" title=""Freccia Decorativa Vintage Look""> Maggiori Informazioni</a>",0.376,1,"Taxable Goods","Catalog, Search",13.9,,,,Freccia-Decorativa-Vintage-Look-Cold beer here,"Freccia Decorativa Vintage Look Cold beer here","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Altri articoli decorativi,Altri,articoli,decorativi,Design Cold beer here,Cold beer here,","Stupisci tutti con la divertente ed originale freccia decorativa Vintage Look! Le pareti di casa tua non resteranno indifferenti a nessuno! Fabbricata in legno",http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,http://dropshipping.bigbuy.eu/imgs/V0000255_89760.jpg,,,,,,"2016-02-29 10:39:59",,,,,,,,,,,,,,,,,"GTIN=4029811346349",20,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0000255_89756.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89761.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89759.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89758.jpg,http://dropshipping.bigbuy.eu/imgs/V0000255_89757.jpg","GTIN=4029811346349",,,,,,,,,, +BB-V0200190,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Bianco","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Bianco,"Ciotola in Bambù TakeTokio Bianco","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Bianco,Bianco,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",0,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200192,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Grigio","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Grigio,"Ciotola in Bambù TakeTokio Grigio","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Grigio,Grigio,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",26,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200191,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Decorazione e Illuminazione,Default Category/Casa Giardino/Decorazione e Illuminazione/Centrotavola e Vasi",base,"Ciotola in Bambù TakeTokio Nero","<a id=""maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""><p>Arricchisci</a> la selezione dei tuoi <strong>utensili da cucina</strong> con la <strong>ciotola in bambù</strong> <strong>TakeTokio</strong>, una <strong>ciotola da cucina</strong> funzionale e dal design moderno è perfetta come <strong>insalatiera</strong>, portafrutta, ecc. Realizzata in pregiato legno di bambù. Dimensioni (diametro x altezza): 25 x 15 cm circa. Diametro della base: 9 cm circa.</p><p><a href=""http://www.taketokio.com/"" target=""_blank""><strong>www.taketokio.com</strong></a></p><p> Dimenzioni per Ciotola in Bambù TakeTokio: </br><ul><li>Altezza: 25 Cm</li><li>Larghezza: 25 Cm</li><li>Profondita': 15 Cm</li><li>Peso: 0.39 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158904577</p>","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc.</br><a href=""#maggiorni_informazioni"" title=""Ciotola in Bambù TakeTokio""> Maggiori Informazioni</a>",0.39,1,"Taxable Goods","Catalog, Search",19.8,,,,Ciotola-in-Bambù-TakeTokio-Nero,"Ciotola in Bambù TakeTokio Nero","Casa Giardino,Casa,Giardino,Decorazione e Illuminazione,Decorazione,Illuminazione,Centrotavola e Vasi,Centrotavola,Vasi,Colore Nero,Nero,","Arricchisci la selezione dei tuoi utensili da cucina con la ciotola in bambù TakeTokio, una ciotola da cucina funzionale e dal design moderno è perfetta come insalatiera, portafrutta, ecc",http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200189_90745.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8718158904577",22,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200189_90746.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90747.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90744.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90743.jpg,http://dropshipping.bigbuy.eu/imgs/V0200189_90742.jpg","GTIN=8718158904577",,,,,,,,,, +BB-V0200360,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Rosa","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Rosa,"Scatola porta Tè Flower Vintage Coconut Rosa","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Rosa,Rosa,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",13,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200361,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Scatola porta Tè Flower Vintage Coconut Azzurro","<a id=""maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""><p>Gli</a> amanti della moda vintage non potranno resistere di fronte all'adorabile <strong>scatola porta tè Flower Vintage Coconut</strong>! Una <strong>scatola vintage</strong> in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi. Dispone di un coperchio in cristallo e vari scompartimenti. Dimensioni: circa 23 x 7 x 23 cm.</p><p><a href=""http://www.vintagecoconut.com"" target=""_blank""><strong>www.vintagecoconut.com</strong></a></p><p> Dimenzioni per Scatola porta Tè Flower Vintage Coconut: </br><ul><li>Altezza: 23 Cm</li><li>Larghezza: 7.1 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.795 Kg</li></ul></p><p>Codice Prodotto (EAN): 8711295889547</p>","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi.</br><a href=""#maggiorni_informazioni"" title=""Scatola porta Tè Flower Vintage Coconut""> Maggiori Informazioni</a>",0.795,1,"Taxable Goods","Catalog, Search",25.9,,,,Scatola-porta-Tè-Flower-Vintage-Coconut-Azzurro,"Scatola porta Tè Flower Vintage Coconut Azzurro","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,Colore Azzurro,Azzurro,","Gli amanti della moda vintage non potranno resistere di fronte all'adorabile scatola porta tè Flower Vintage Coconut! Una scatola vintage in legno ottima come decorazione e utilissima per sistemare le bustine di tè o di altri infusi",http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200328_92648.jpg,,,,,,"-0001-11-30 00:00:00",,,,,,,,,,,,,,,,,"GTIN=8711295889547",21,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200328_92649.jpg,http://dropshipping.bigbuy.eu/imgs/V0200328_92647.jpg","GTIN=8711295889547",,,,,,,,,, +BB-V0200353,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Giardino e Terrazza,Default Category/Casa Giardino/Giardino e Terrazza/Barbecue",base,"Ventilatore a Pistola classico per Barbecue BBQ Classics","<a id=""maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""><p>Utilizza</a> i migliori barbecue alimentando il fuoco con il <strong>ventilatore a pistola classico per babecue BBQ Classics</strong>! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</p><p><a href=""http://www.bbqclassics.com"" target=""_blank""><strong>www.bbqclassics.com</strong></a></p><ul><li>Realizzato in plastica e metallo</li><li>Dimensioni: 25 x 18 x 4 cm circa</li></ul><p> Dimenzioni per Ventilatore a Pistola classico per Barbecue BBQ Classics: </br><ul><li>Altezza: 5.5 Cm</li><li>Larghezza: 11 Cm</li><li>Profondita': 22 Cm</li><li>Peso: 0.167 Kg</li></ul></p><p>Codice Prodotto (EAN): 8718158032706</p>","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria.</br><a href=""#maggiorni_informazioni"" title=""Ventilatore a Pistola classico per Barbecue BBQ Classics""> Maggiori Informazioni</a>",0.167,1,"Taxable Goods","Catalog, Search",9.3,,,,Ventilatore-a-Pistola-classico-per-Barbecue-BBQ-Classics,"Ventilatore a Pistola classico per Barbecue BBQ Classics","Casa Giardino,Casa,Giardino,Giardino e Terrazza,Giardino,Terrazza,Barbecue,","Utilizza i migliori barbecue alimentando il fuoco con il ventilatore a pistola classico per babecue BBQ Classics! Basterà solo fare una leggera pressione sul pulsante per far uscire l'aria",http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,http://dropshipping.bigbuy.eu/imgs/V0200353_92695.jpg,,,,,,"2016-09-13 09:56:07",,,,,,,,,,,,,,,,,"GTIN=8718158032706",60,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V0200353_92696.jpg,http://dropshipping.bigbuy.eu/imgs/V0200353_92694.jpg","GTIN=8718158032706",,,,,,,,,, +BB-V1600123,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""><p>Non</a> c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente. Con il <strong>contenitore portagiochi Frozen (32 x 23 cm)</strong> sarà semplicissimo!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni aprossimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> </p><p> Dimenzioni per Contenitore Portagiochi Frozen (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766006</p>","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Frozen-(32-x-23-cm),"Contenitore Portagiochi Frozen (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Non c'è nulla di meglio che tenere le camere dei più piccini in ordine in modo originale e divertente",http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600123_93002.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766006",48,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600123_93005.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93004.jpg,http://dropshipping.bigbuy.eu/imgs/V1600123_93003.jpg","GTIN=8412842766006",,,,,,,,,, +BB-V1600124,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (32 x 23 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""><p>Desideri</a> sorprendere i più piccini con un regalo molto originale? Il <strong>contenitore portagiochi Spiderman (32 x 23 cm)</strong> decorerà e metterà in ordine le loro camerette.</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni approssimative: 32 x 15 x 23 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (32 x 23 cm): </br><ul><li>Altezza: 15 Cm</li><li>Larghezza: 34 Cm</li><li>Profondita': 23 Cm</li><li>Peso: 0.331 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766037</p>","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette.</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (32 x 23 cm)""> Maggiori Informazioni</a>",0.331,1,"Taxable Goods","Catalog, Search",21.9,,,,Contenitore-Portagiochi-Spiderman--(32-x-23-cm),"Contenitore Portagiochi Spiderman (32 x 23 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Desideri sorprendere i più piccini con un regalo molto originale? Il contenitore portagiochi Spiderman (32 x 23 cm) decorerà e metterà in ordine le loro camerette",http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600124_93006.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766037",52,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600124_93008.jpg,http://dropshipping.bigbuy.eu/imgs/V1600124_93007.jpg","GTIN=8412842766037",,,,,,,,,, +BB-V1600125,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Frozen (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""><p>Insegna</a> ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del <strong>contenitore portagiochi Frozen (45 x 32 cm)</strong>. Il <strong>portagiocattoli</strong> che tutte le bambine sognano!</p><ul><li>Realizzato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età consigliata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Frozen (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766129</p>","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Frozen (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Frozen-(45-x-32-cm),"Contenitore Portagiochi Frozen (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","Insegna ai tuoi bambini a tenere i giocattoli conservati ben in ordine con l'aiuto del contenitore portagiochi Frozen (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600125_93010.jpg,,,,,,"2016-08-08 21:04:27",,,,,,,,,,,,,,,,,"GTIN=8412842766129",17,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600125_93011.jpg,http://dropshipping.bigbuy.eu/imgs/V1600125_93009.jpg","GTIN=8412842766129",,,,,,,,,, +BB-V1600126,,Default,simple,"Default Category/Casa Giardino,Default Category/Casa Giardino/Arredamento,Default Category/Casa Giardino/Arredamento/Soluzioni per Organizzare",base,"Contenitore Portagiochi Spiderman (45 x 32 cm)","<a id=""maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""><p>I</a> piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> Spiderman</strong><strong> (45 x 32 cm)</strong>. Il <strong>c<strong>ontenitore <strong>portagiochi</strong></strong> </strong>preferito dai bambini!</p><ul><li>Fabbricato in polipropilene</li><li>Dimensioni: circa 45 x 22 x 32 cm</li><li>Età raccomandata: +3 anni</li></ul><p> Dimenzioni per Contenitore Portagiochi Spiderman (45 x 32 cm): </br><ul><li>Altezza: 22 Cm</li><li>Larghezza: 37 Cm</li><li>Profondita': 45 Cm</li><li>Peso: 0.775 Kg</li></ul></p><p>Codice Prodotto (EAN): 8412842766150</p>","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm).</br><a href=""#maggiorni_informazioni"" title=""Contenitore Portagiochi Spiderman (45 x 32 cm)""> Maggiori Informazioni</a>",0.775,1,"Taxable Goods","Catalog, Search",39.6,,,,Contenitore-Portagiochi-Spiderman-(45-x-32-cm),"Contenitore Portagiochi Spiderman (45 x 32 cm)","Casa Giardino,Casa,Giardino,Arredamento,Soluzioni per Organizzare,Soluzioni,Organizzare,","I piccoli di casa ora possono ordinare e riporre i loro giocattoli facilmente grazie al contenitore portagiochi Spiderman (45 x 32 cm)",http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,http://dropshipping.bigbuy.eu/imgs/V1600126_93012.jpg,,,,,,"2016-08-08 21:09:24",,,,,,,,,,,,,,,,,"GTIN=8412842766150",18,0,1,0,1,1,1,1,10000,1,1,,1,0,1,1,1,1,0,0,0,,,,,,,"http://dropshipping.bigbuy.eu/imgs/V1600126_93014.jpg,http://dropshipping.bigbuy.eu/imgs/V1600126_93013.jpg","GTIN=8412842766150",,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv b/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv new file mode 100644 index 0000000000000..95e359ce7a1d9 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_import_products_url_rewrite.csv @@ -0,0 +1,2 @@ +sku,url_key +SimpleProductForTest1,SimpleProductAfterImport1-new diff --git a/dev/tests/acceptance/tests/_data/tablerates.csv b/dev/tests/acceptance/tests/_data/tablerates.csv new file mode 100644 index 0000000000000..ddc591798e3cc --- /dev/null +++ b/dev/tests/acceptance/tests/_data/tablerates.csv @@ -0,0 +1,21 @@ +Country,Region/State,Zip/Postal Code,Weight (and above),Shipping Price +ASM,*,*,0,9.95 +FSM,*,*,0,9.95 +GUM,*,*,0,9.95 +MHL,*,*,0,9.95 +MNP,*,*,0,9.95 +PLW,*,*,0,9.95 +USA,AA,*,0,9.95 +USA,AE,*,0,9.95 +USA,AK,*,0,9.95 +USA,AP,*,0,9.95 +USA,AS,*,0,9.95 +USA,FM,*,0,9.95 +USA,GU,*,0,9.95 +USA,HI,*,0,9.95 +USA,MH,*,0,9.95 +USA,MP,*,0,9.95 +USA,PR,*,0,9.95 +USA,PW,*,0,9.95 +USA,VI,*,0,9.95 +VIR,*,*,0,9.95 \ No newline at end of file diff --git a/dev/tests/acceptance/tests/_data/usa_tablerates.csv b/dev/tests/acceptance/tests/_data/usa_tablerates.csv new file mode 100644 index 0000000000000..d5a59ae6bccf2 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/usa_tablerates.csv @@ -0,0 +1,13 @@ +Country,Region/State,"Zip/Postal Code","Order Subtotal (and above)","Shipping Price" +USA,*,*,0.0000,7.9900 +USA,*,*,7.0000,6.9900 +USA,*,*,13.0000,5.9900 +USA,*,*,25.9900,4.9900 +USA,AK,*,0.0000,8.9900 +USA,AK,*,7.0000,7.9900 +USA,AK,*,13.0000,6.9900 +USA,AK,*,25.9900,5.9900 +USA,HI,*,0.0000,8.9900 +USA,HI,*,7.0000,7.9900 +USA,HI,*,13.0000,6.9900 +USA,HI,*,25.9900,5.9900 diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml index f0bfec543f281..cac9d0c3cb55f 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml @@ -27,4 +27,23 @@ <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> </test> + <test name="EndToEndB2CGuestUserMysqlTest"> + <!-- Search configurable product --> + <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="searchAssertFilterCategoryConfigProduct" after="commentSearchConfigurableProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$createConfigProduct.name$$)}}" userInput="src" stepKey="searchGrabConfigProductImageSrc" after="searchAssertFilterCategoryConfigProduct"/> + <assertNotRegExp expected="'/placeholder\/small_image\.jpg/'" actual="$searchGrabConfigProductImageSrc" stepKey="searchAssertConfigProductImageNotDefault" after="searchGrabConfigProductImageSrc"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="searchClickConfigProductView" after="searchAssertConfigProductImageNotDefault"/> + <actionGroup ref="StorefrontCheckConfigurableProduct" stepKey="searchAssertConfigProductPage" after="searchClickConfigProductView"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="optionProduct" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> + <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> + </test> </tests> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/etc/module.xml new file mode 100644 index 0000000000000..bae0739d237e1 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleCatalogSearch"> + <sequence> + <module name="Magento_CatalogSearch"/> + </sequence> + </module> +</config> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php new file mode 100644 index 0000000000000..78fb97a9e1134 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleCatalogSearch/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch', __DIR__); +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index a5be493032836..8061cb138660d 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -10,6 +10,7 @@ use Magento\Config\Model\Config; use Magento\Config\Model\ResourceModel\Config as ConfigResource; use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\TestCase; @@ -156,4 +157,31 @@ private function getStoreIdByCode(string $storeCode): int $store = $storeManager->getStore($storeCode); return (int)$store->getId(); } + + /** + * @inheritDoc + */ + protected function _setConfigValue($configPath, $value, $storeCode = false) + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + if ($storeCode === false) { + $objectManager->get( + \Magento\TestFramework\App\ApiMutableScopeConfig::class + )->setValue( + $configPath, + $value, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + + return; + } + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\TestFramework\App\ApiMutableScopeConfig::class + )->setValue( + $configPath, + $value, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeCode + ); + } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php similarity index 86% rename from dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php rename to dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php index efcb5be34e594..fa0cebece9a96 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/App/MutableScopeConfig.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/App/ApiMutableScopeConfig.php @@ -17,7 +17,7 @@ /** * @inheritdoc */ -class MutableScopeConfig implements MutableScopeConfigInterface +class ApiMutableScopeConfig implements MutableScopeConfigInterface { /** * @var Config @@ -56,7 +56,6 @@ public function setValue( /** * Clean app config cache * - * @param string|null $type * @return void */ public function clean() @@ -89,19 +88,13 @@ private function getTestAppConfig() private function persistConfig($path, $value, $scopeType, $scopeCode): void { $pathParts = explode('/', $path); - $store = ''; - if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE) { - if ($scopeCode !== null) { - $store = ObjectManager::getInstance() + $store = 0; + if ($scopeType === \Magento\Store\Model\ScopeInterface::SCOPE_STORE + && $scopeCode !== null) { + $store = ObjectManager::getInstance() ->get(\Magento\Store\Api\StoreRepositoryInterface::class) ->get($scopeCode) ->getId(); - } else { - $store = ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore() - ->getId(); - } } $configData = [ 'section' => $pathParts[0], diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php index 9ad051b686d47..6400a61b3ef35 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Webapi\Exception as WebapiException; use Magento\Webapi\Model\Soap\Fault; use Magento\TestFramework\Helper\Bootstrap; @@ -102,9 +103,11 @@ abstract class WebapiAbstract extends \PHPUnit\Framework\TestCase /** * Initialize fixture namespaces. + * //phpcs:disable */ public static function setUpBeforeClass() { + //phpcs:enable parent::setUpBeforeClass(); self::_setFixtureNamespace(); } @@ -113,9 +116,11 @@ public static function setUpBeforeClass() * Run garbage collector for cleaning memory * * @return void + * //phpcs:disable */ public static function tearDownAfterClass() { + //phpcs:enable //clear garbage in memory gc_collect_cycles(); @@ -133,8 +138,7 @@ public static function tearDownAfterClass() } /** - * Call safe delete for models which added to delete list - * Restore config values changed during the test + * Call safe delete for models which added to delete list, Restore config values changed during the test * * @return void */ @@ -178,6 +182,8 @@ protected function _webApiCall( /** * Mark test to be executed for SOAP adapter only. + * + * @param ?string $message */ protected function _markTestAsSoapOnly($message = null) { @@ -188,6 +194,8 @@ protected function _markTestAsSoapOnly($message = null) /** * Mark test to be executed for REST adapter only. + * + * @param ?string $message */ protected function _markTestAsRestOnly($message = null) { @@ -203,9 +211,11 @@ protected function _markTestAsRestOnly($message = null) * @param mixed $fixture * @param int $tearDown * @return void + * //phpcs:disable */ public static function setFixture($key, $fixture, $tearDown = self::AUTO_TEAR_DOWN_AFTER_METHOD) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (!isset(self::$_fixtures[$fixturesNamespace])) { self::$_fixtures[$fixturesNamespace] = []; @@ -231,9 +241,11 @@ public static function setFixture($key, $fixture, $tearDown = self::AUTO_TEAR_DO * * @param string $key * @return mixed + * //phpcs:disable */ public static function getFixture($key) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) { return self::$_fixtures[$fixturesNamespace][$key]; @@ -247,9 +259,11 @@ public static function getFixture($key) * @param \Magento\Framework\Model\AbstractModel $model * @param bool $secure * @return \Magento\TestFramework\TestCase\WebapiAbstract + * //phpcs:disable */ public static function callModelDelete($model, $secure = false) { + //phpcs:enable if ($model instanceof \Magento\Framework\Model\AbstractModel && $model->getId()) { if ($secure) { self::_enableSecureArea(); @@ -300,9 +314,11 @@ protected function _getWebApiAdapter($webApiAdapterCode) * Set fixtures namespace * * @throws \RuntimeException + * //phpcs:disable */ protected static function _setFixtureNamespace() { + //phpcs:enable if (self::$_fixturesNamespace !== null) { throw new \RuntimeException('Fixture namespace is already set.'); } @@ -311,9 +327,11 @@ protected static function _setFixtureNamespace() /** * Unset fixtures namespace + * //phpcs:disable */ protected static function _unsetFixtureNamespace() { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); unset(self::$_fixtures[$fixturesNamespace]); self::$_fixturesNamespace = null; @@ -324,9 +342,12 @@ protected static function _unsetFixtureNamespace() * * @throws \RuntimeException * @return string + * //phpcs:disable */ protected static function _getFixtureNamespace() { + //phpcs:enable + $fixtureNamespace = self::$_fixturesNamespace; if ($fixtureNamespace === null) { throw new \RuntimeException('Fixture namespace must be set.'); @@ -339,9 +360,12 @@ protected static function _getFixtureNamespace() * * @param bool $flag * @return void + * //phpcs:disable */ protected static function _enableSecureArea($flag = true) { + //phpcs:enable + /** @var $objectManager \Magento\TestFramework\ObjectManager */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -388,9 +412,11 @@ protected function _assertMessagesEqual($expectedMessages, $receivedMessages) * Delete array of fixtures * * @param array $fixtures + * //phpcs:disable */ protected static function _deleteFixtures($fixtures) { + //phpcs:enable foreach ($fixtures as $fixture) { self::deleteFixture($fixture, true); } @@ -402,9 +428,11 @@ protected static function _deleteFixtures($fixtures) * @param string $key * @param bool $secure * @return void + * //phpcs:disable */ public static function deleteFixture($key, $secure = false) { + //phpcs:enable $fixturesNamespace = self::_getFixtureNamespace(); if (array_key_exists($key, self::$_fixtures[$fixturesNamespace])) { self::callModelDelete(self::$_fixtures[$fixturesNamespace][$key], $secure); @@ -456,11 +484,11 @@ protected function _cleanAppConfigCache() /** * Update application config data * - * @param string $path Config path with the form "section/group/node" - * @param string|int|null $value Value of config item - * @param bool $cleanAppCache If TRUE application cache will be refreshed - * @param bool $updateLocalConfig If TRUE local config object will be updated too - * @param bool $restore If TRUE config value will be restored after test run + * @param string $path Config path with the form "section/group/node" + * @param string|int|null $value Value of config item + * @param bool $cleanAppCache If TRUE application cache will be refreshed + * @param bool $updateLocalConfig If TRUE local config object will be updated too + * @param bool $restore If TRUE config value will be restored after test run * @return \Magento\TestFramework\TestCase\WebapiAbstract * @throws \RuntimeException */ @@ -520,6 +548,8 @@ protected function _restoreAppConfig() } /** + * Process rest exception result. + * * @param \Exception $e * @return array * <pre> ex. @@ -666,11 +696,19 @@ protected function _checkWrappedErrors($expectedWrappedErrors, $errorDetails) } /** + * Get actual wrapped errors. + * * @param \stdClass $errorNode * @return array */ private function getActualWrappedErrors(\stdClass $errorNode) { + if (!isset($errorNode->parameters)) { + return [ + 'message' => $errorNode->message, + ]; + } + $actualParameters = []; $parameterNode = $errorNode->parameters->parameter; if (is_array($parameterNode)) { @@ -686,4 +724,42 @@ private function getActualWrappedErrors(\stdClass $errorNode) 'params' => $actualParameters, ]; } + + /** + * Assert webapi errors. + * + * @param array $serviceInfo + * @param array $data + * @param array $expectedErrorData + * @return void + * @throws \Exception + */ + protected function assertWebApiCallErrors(array $serviceInfo, array $data, array $expectedErrorData) + { + try { + $this->_webApiCall($serviceInfo, $data); + $this->fail('Expected throwing exception'); + } catch (\Exception $e) { + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_REST) { + self::assertEquals($expectedErrorData, $this->processRestExceptionResult($e)); + self::assertEquals(WebapiException::HTTP_BAD_REQUEST, $e->getCode()); + } elseif (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $this->assertInstanceOf('SoapFault', $e); + $expectedWrappedErrors = []; + foreach ($expectedErrorData['errors'] as $error) { + // @see \Magento\TestFramework\TestCase\WebapiAbstract::getActualWrappedErrors() + $expectedWrappedError = [ + 'message' => $error['message'], + ]; + if (isset($error['parameters'])) { + $expectedWrappedError['params'] = $error['parameters']; + } + $expectedWrappedErrors[] = $expectedWrappedError; + } + $this->checkSoapFault($e, $expectedErrorData['message'], 'env:Sender', [], $expectedWrappedErrors); + } else { + throw $e; + } + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php index 9a6520a3ab458..1cd299149507c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php @@ -4,16 +4,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Api; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ProductFactory; +use Magento\TestFramework\ObjectManager; +use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; /** * Class ProductAttributeMediaGalleryManagementInterfaceTest */ -class ProductAttributeMediaGalleryManagementInterfaceTest extends \Magento\TestFramework\TestCase\WebapiAbstract +class ProductAttributeMediaGalleryManagementInterfaceTest extends WebapiAbstract { /** * Default create service request information (product with SKU 'simple' is used) @@ -41,12 +48,15 @@ class ProductAttributeMediaGalleryManagementInterfaceTest extends \Magento\TestF */ protected $testImagePath; + /** + * @inheritDoc + */ protected function setUp() { $this->createServiceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/simple/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -58,7 +68,7 @@ protected function setUp() $this->updateServiceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/simple/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -66,9 +76,10 @@ protected function setUp() 'operation' => 'catalogProductAttributeMediaGalleryManagementV1Update', ], ]; + $this->deleteServiceInfo = [ 'rest' => [ - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -76,6 +87,7 @@ protected function setUp() 'operation' => 'catalogProductAttributeMediaGalleryManagementV1Remove', ], ]; + $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; } @@ -87,7 +99,8 @@ protected function setUp() protected function getTargetSimpleProduct() { $objectManager = Bootstrap::getObjectManager(); - return $objectManager->get(\Magento\Catalog\Model\ProductFactory::class)->create()->load(1); + + return $objectManager->get(ProductFactory::class)->create()->load(1); } /** @@ -101,17 +114,20 @@ protected function getTargetGalleryEntryId() { $mediaGallery = $this->getTargetSimpleProduct()->getData('media_gallery'); $image = array_shift($mediaGallery['images']); + return (int)$image['value_id']; } /** + * Test create() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreate() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -138,13 +154,15 @@ public function testCreate() } /** + * Test create() method without file + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreateWithoutFileExtension() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -171,13 +189,15 @@ public function testCreateWithoutFileExtension() } /** + * Test create() method with not default store id + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testCreateWithNotDefaultStoreId() { $requestData = [ 'id' => null, - 'media_type' => \Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter::MEDIA_TYPE_CODE, + 'media_type' => ImageEntryConverter::MEDIA_TYPE_CODE, 'label' => 'Image Text', 'position' => 1, 'types' => ['image'], @@ -215,6 +235,8 @@ public function testCreateWithNotDefaultStoreId() } /** + * Test update() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testUpdate() @@ -253,6 +275,8 @@ public function testUpdate() } /** + * Test update() method with not default store id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testUpdateWithNotDefaultStoreId() @@ -291,7 +315,9 @@ public function testUpdateWithNotDefaultStoreId() } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + * Test delete() method + * + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image_without_types.php */ public function testDelete() { @@ -309,6 +335,8 @@ public function testDelete() } /** + * Test create() method if provided content is not base64 encoded + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image content must be valid base64 encoded data. @@ -334,6 +362,8 @@ public function testCreateThrowsExceptionIfProvidedContentIsNotBase64Encoded() } /** + * Test create() method if provided content is not an image + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image content must be valid base64 encoded data. @@ -359,6 +389,8 @@ public function testCreateThrowsExceptionIfProvidedContentIsNotAnImage() } /** + * Test create() method if provided image has wrong MIME type + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage The image MIME type is not valid or not supported. @@ -384,6 +416,8 @@ public function testCreateThrowsExceptionIfProvidedImageHasWrongMimeType() } /** + * Test create method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -409,6 +443,8 @@ public function testCreateThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test create() method if provided image name contains forbidden characters + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php * @expectedException \Exception * @expectedExceptionMessage Provided image name contains forbidden characters. @@ -433,6 +469,8 @@ public function testCreateThrowsExceptionIfProvidedImageNameContainsForbiddenCha } /** + * Test update() method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -456,6 +494,8 @@ public function testUpdateThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test update() method if there is no image with given id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php * @expectedException \Exception * @expectedExceptionMessage No image with the provided ID was found. Verify the ID and try again. @@ -481,6 +521,8 @@ public function testUpdateThrowsExceptionIfThereIsNoImageWithGivenId() } /** + * Test delete() method if target product does not exist + * * @expectedException \Exception * @expectedExceptionMessage The product that was requested doesn't exist. Verify the product and try again. */ @@ -496,6 +538,8 @@ public function testDeleteThrowsExceptionIfTargetProductDoesNotExist() } /** + * Test delete() method if there is no image with given id + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php * @expectedException \Exception * @expectedExceptionMessage No image with the provided ID was found. Verify the ID and try again. @@ -512,15 +556,17 @@ public function testDeleteThrowsExceptionIfThereIsNoImageWithGivenId() } /** + * Test get() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testGet() { $productSku = 'simple'; - $objectManager = \Magento\TestFramework\ObjectManager::getInstance(); - /** @var \Magento\Catalog\Model\ProductRepository $repository */ - $repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + $objectManager = ObjectManager::getInstance(); + /** @var ProductRepository $repository */ + $repository = $objectManager->create(ProductRepository::class); $product = $repository->get($productSku); $image = current($product->getMediaGallery('images')); $imageId = $image['value_id']; @@ -537,7 +583,7 @@ public function testGet() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . $productSku . '/media/' . $imageId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -560,6 +606,8 @@ public function testGet() } /** + * Test getList() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ public function testGetList() @@ -568,7 +616,7 @@ public function testGetList() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . urlencode($productSku) . '/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -591,13 +639,16 @@ public function testGetList() $this->assertContains('thumbnail', $imageTypes); } + /** + * Test getList() method for absent sku + */ public function testGetListForAbsentSku() { $productSku = 'absent_sku_' . time(); $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products/' . urlencode($productSku) . '/media', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => 'catalogProductAttributeMediaGalleryManagementV1', @@ -622,6 +673,8 @@ public function testGetListForAbsentSku() } /** + * Test addProductVideo() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testAddProductVideo() diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 3e935e1d7ae9b..5eaa8f68611be 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -24,6 +24,8 @@ use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; /** + * Test for \Magento\Catalog\Api\ProductRepositoryInterface + * * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -56,6 +58,8 @@ class ProductRepositoryInterfaceTest extends WebapiAbstract ]; /** + * Test get() method + * * @magentoApiDataFixture Magento/Catalog/_files/products_related.php */ public function testGet() @@ -69,6 +73,8 @@ public function testGet() } /** + * Get product + * * @param string $sku * @param string|null $storeCode * @return array|bool|float|int|string @@ -86,11 +92,14 @@ protected function getProduct($sku, $storeCode = null) 'operation' => self::SERVICE_NAME . 'Get', ], ]; - $response = $this->_webApiCall($serviceInfo, ['sku' => $sku], null, $storeCode); + return $response; } + /** + * Test get() method with invalid sku + */ public function testGetNoSuchEntityException() { $invalidSku = '(nonExistingSku)'; @@ -125,6 +134,8 @@ public function testGetNoSuchEntityException() } /** + * Product creation provider + * * @return array */ public function productCreationProvider() @@ -135,6 +146,7 @@ public function productCreationProvider() $data ); }; + return [ [$productBuilder([ProductInterface::TYPE_ID => 'simple', ProductInterface::SKU => 'psku-test-1'])], [$productBuilder([ProductInterface::TYPE_ID => 'virtual', ProductInterface::SKU => 'psku-test-2'])], @@ -161,6 +173,7 @@ private function loadWebsiteByCode($websiteCode) /** * Test removing association between product and website 1 + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php */ public function testUpdateWithDeleteWebsites() @@ -184,6 +197,7 @@ public function testUpdateWithDeleteWebsites() /** * Test removing all website associations + * * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php */ public function testDeleteAllWebsiteAssociations() @@ -202,6 +216,8 @@ public function testDeleteAllWebsiteAssociations() } /** + * Test create() method with multiple websites + * * @magentoApiDataFixture Magento/Catalog/_files/second_website.php */ public function testCreateWithMultipleWebsites() @@ -305,6 +321,8 @@ public function testUpdateWithoutWebsiteIds() } /** + * Test create() method + * * @dataProvider productCreationProvider */ public function testCreate($product) @@ -372,6 +390,9 @@ public function testCreateAllStoreCodeForSingleWebsite($fixtureProduct) $this->deleteProduct($fixtureProduct[ProductInterface::SKU]); } + /** + * Test create() method with invalid price format + */ public function testCreateInvalidPriceFormat() { $this->_markTestAsRestOnly("In case of SOAP type casting is handled by PHP SoapServer, no need to test it"); @@ -408,6 +429,9 @@ public function testDeleteAllStoreCode($fixtureProduct) $this->getProduct($sku); } + /** + * Test product links + */ public function testProductLinks() { // Create simple product @@ -441,7 +465,6 @@ public function testProductLinks() ProductInterface::TYPE_ID => 'simple', ProductInterface::PRICE => 100, ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', ProductInterface::ATTRIBUTE_SET_ID => 4, "product_links" => [$productLinkData] ]; @@ -504,6 +527,8 @@ public function testProductLinks() } /** + * Get options data + * * @param string $productSku * @return array */ @@ -543,6 +568,9 @@ protected function getOptionsData($productSku) ]; } + /** + * Test product options + */ public function testProductOptions() { //Create product with options @@ -604,6 +632,9 @@ public function testProductOptions() $this->deleteProduct($productData[ProductInterface::SKU]); } + /** + * Test product with media gallery + */ public function testProductWithMediaGallery() { $testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; @@ -635,7 +666,7 @@ public function testProductWithMediaGallery() 'position' => 2, 'media_type' => 'image', 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename2, ], ]; @@ -648,7 +679,7 @@ public function testProductWithMediaGallery() 'label' => 'tiny1_new_label', 'position' => 1, 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename1, ], ]; @@ -662,7 +693,7 @@ public function testProductWithMediaGallery() 'media_type' => 'image', 'position' => 1, 'disabled' => false, - 'types' => ['image', 'small_image'], + 'types' => [], 'file' => '/t/i/' . $filename1, ] ]; @@ -682,6 +713,8 @@ public function testProductWithMediaGallery() } /** + * Test update() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testUpdate() @@ -725,13 +758,16 @@ public function testUpdateWithExtensionAttributes(): void } /** + * Update product + * * @param array $product * @return array|bool|float|int|string */ protected function updateProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $countOfProductCustomAttributes = sizeof($product['custom_attributes']); + for ($i = 0; $i < $countOfProductCustomAttributes; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { @@ -761,6 +797,8 @@ protected function updateProduct($product) } /** + * Test delete() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testDelete() @@ -770,6 +808,8 @@ public function testDelete() } /** + * Test getList() method + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testGetList() @@ -832,6 +872,8 @@ public function testGetList() } /** + * Test getList() method with additional params + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testGetListWithAdditionalParams() @@ -871,6 +913,8 @@ public function testGetListWithAdditionalParams() } /** + * Test getList() method with filtering by website + * * @magentoApiDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php * @return void */ @@ -958,6 +1002,11 @@ public function testGetListWithFilteringByStore(array $searchCriteria, array $sk } } + /** + * Test getList() method with filtering by store data provider + * + * @return array + */ public function testGetListWithFilteringByStoreDataProvider() { return [ @@ -997,6 +1046,8 @@ public function testGetListWithFilteringByStoreDataProvider() } /** + * Test getList() method with multiple filter groups and sorting and pagination + * * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php */ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() @@ -1066,6 +1117,8 @@ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() } /** + * Convert custom attributes to associative array + * * @param $customAttributes * @return array */ @@ -1075,10 +1128,13 @@ protected function convertCustomAttributesToAssociativeArray($customAttributes) foreach ($customAttributes as $customAttribute) { $converted[$customAttribute['attribute_code']] = $customAttribute['value']; } + return $converted; } /** + * Convert associative array to custom attributes + * * @param $data * @return array */ @@ -1088,10 +1144,13 @@ protected function convertAssociativeArrayToCustomAttributes($data) foreach ($data as $attributeCode => $attributeValue) { $customAttributes[] = ['attribute_code' => $attributeCode, 'value' => $attributeValue]; } + return $customAttributes; } /** + * Test eav attributes + * * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ public function testEavAttributes() @@ -1135,7 +1194,6 @@ protected function getSimpleProductData($productData = []) ProductInterface::TYPE_ID => 'simple', ProductInterface::PRICE => 3.62, ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', ProductInterface::ATTRIBUTE_SET_ID => 4, 'custom_attributes' => [ ['attribute_code' => 'cost', 'value' => ''], @@ -1145,6 +1203,8 @@ protected function getSimpleProductData($productData = []) } /** + * Save Product + * * @param $product * @param string|null $storeCode * @return mixed @@ -1152,7 +1212,8 @@ protected function getSimpleProductData($productData = []) protected function saveProduct($product, $storeCode = null) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + $countOfProductCustomAttributes = sizeof($product['custom_attributes']); + for ($i = 0; $i < $countOfProductCustomAttributes; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { @@ -1172,6 +1233,7 @@ protected function saveProduct($product, $storeCode = null) ], ]; $requestData = ['product' => $product]; + return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode); } @@ -1199,6 +1261,9 @@ protected function deleteProduct($sku) $this->_webApiCall($serviceInfo, ['sku' => $sku]) : $this->_webApiCall($serviceInfo); } + /** + * Test tier prices + */ public function testTierPrices() { // create a product with tier prices @@ -1283,6 +1348,8 @@ public function testTierPrices() } /** + * Get stock item data + * * @return array */ private function getStockItemData() @@ -1315,6 +1382,8 @@ private function getStockItemData() } /** + * Test product category links + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testProductCategoryLinks() @@ -1337,6 +1406,8 @@ public function testProductCategoryLinks() } /** + * Test update product category without categories + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksNullOrNotExists() @@ -1358,6 +1429,8 @@ public function testUpdateProductCategoryLinksNullOrNotExists() } /** + * Test update product category links position + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksPosistion() @@ -1375,6 +1448,8 @@ public function testUpdateProductCategoryLinksPosistion() } /** + * Test update product category links unassing + * * @magentoApiDataFixture Magento/Catalog/_files/category_product.php */ public function testUpdateProductCategoryLinksUnassign() @@ -1387,6 +1462,8 @@ public function testUpdateProductCategoryLinksUnassign() } /** + * Get media gallery data + * * @param $filename1 * @param $encodedImage * @param $filename2 @@ -1412,7 +1489,7 @@ private function getMediaGalleryData($filename1, $encodedImage, $filename2) 'media_type' => 'image', 'disabled' => false, 'label' => 'tiny2', - 'types' => ['image', 'small_image'], + 'types' => [], 'content' => [ 'type' => 'image/jpeg', 'name' => $filename2, @@ -1422,6 +1499,9 @@ private function getMediaGalleryData($filename1, $encodedImage, $filename2) ]; } + /** + * Test special price + */ public function testSpecialPrice() { $productData = $this->getSimpleProductData(); @@ -1471,6 +1551,9 @@ public function testResetSpecialPrice() $this->assertFalse(array_key_exists(self::KEY_SPECIAL_PRICE, $customAttributes)); } + /** + * Test update status + */ public function testUpdateStatus() { // Create simple product @@ -1543,6 +1626,8 @@ public function testUpdateMultiselectAttributes() } /** + * Get attribute options + * * @param string $attributeCode * @return array|bool|float|int|string */ @@ -1564,6 +1649,8 @@ private function getAttributeOptions($attributeCode) } /** + * Assert multiselect value + * * @param string $productSku * @param string $multiselectAttributeCode * @param string $expectedMultiselectValue diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php index 844230f4e3337..65e1e65e36beb 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryMultiCurrencyTest.php @@ -17,6 +17,7 @@ class ProductRepositoryMultiCurrencyTest extends WebapiAbstract const WEBSITES_RESOURCE_PATH = '/V1/store/websites'; /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Store/_files/second_website_with_second_currency.php * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php index 65ea71bd34937..e6bc36684ed80 100644 --- a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php @@ -38,7 +38,7 @@ public function testCatalogSearch() ] ] ], - 'page_size' => 20000000000000, + 'page_size' => 999, 'current_page' => 0, ], ]; @@ -66,7 +66,15 @@ public function testCatalogSearch() $this->assertTrue(count($response['items']) > 0); $this->assertNotNull($response['items'][0]['id']); - $this->assertEquals('score', $response['items'][0]['custom_attributes'][0]['attribute_code']); + $this->assertTrue( + in_array( + $response['items'][0]['custom_attributes'][0]['attribute_code'], + [ + 'score', // mysql + '_score' // elasticsearch score + ] + ) + ); $this->assertTrue($response['items'][0]['custom_attributes'][0]['value'] > 0); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php index 5f70cf4fd6687..0ca1be775258d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Customer/SetPaymentMethodTest.php @@ -156,9 +156,25 @@ function (string $maskedQuoteId) { ], [ function (string $maskedQuoteId) { - return $this->getInvalidAcceptJsInput($maskedQuoteId); + return $this->getEmptyAcceptJsInput($maskedQuoteId); }, - 'for "authorizenet_acceptjs" is missing.' + 'for "authorizenet_acceptjs" is missing.', + ], + [ + function (string $maskedQuoteId) { + return $this->getMissingCcLastFourAcceptJsInput( + $maskedQuoteId, + static::VALID_DESCRIPTOR, + static::VALID_NONCE + ); + }, + 'parameter "cc_last_4" for "authorizenet_acceptjs" is missing', + ], + [ + function (string $maskedQuoteId) { + return $this->getMissingOpaqueDataValueAcceptJsInput($maskedQuoteId, static::VALID_DESCRIPTOR); + }, + 'parameter "opaque_data_value" for "authorizenet_acceptjs" is missing', ], ]; } @@ -190,12 +206,12 @@ private function getInvalidSetPaymentMutation(string $maskedQuoteId): string } /** - * Get setPaymentMethodOnCart missing require additional data properties + * Get setPaymentMethodOnCart missing required additional data properties * * @param string $maskedQuoteId * @return string */ - private function getInvalidAcceptJsInput(string $maskedQuoteId): string + private function getEmptyAcceptJsInput(string $maskedQuoteId): string { return <<<QUERY mutation { @@ -216,12 +232,72 @@ private function getInvalidAcceptJsInput(string $maskedQuoteId): string QUERY; } + /** + * Get setPaymentMethodOnCart missing required additional data properties + * + * @param string $maskedQuoteId + * @return string + */ + private function getMissingCcLastFourAcceptJsInput(string $maskedQuoteId, string $descriptor, string $nonce): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + authorizenet_acceptjs:{ + opaque_data_descriptor: "{$descriptor}" + opaque_data_value: "{$nonce}" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * Get setPaymentMethodOnCart missing required additional data properties + * + * @param string $maskedQuoteId + * @return string + */ + private function getMissingOpaqueDataValueAcceptJsInput(string $maskedQuoteId, string $descriptor): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"authorizenet_acceptjs" + authorizenet_acceptjs:{ + opaque_data_descriptor: "{$descriptor}" + cc_last_4: 1111 + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + private function assertPlaceOrderResponse(array $response, string $reservedOrderId): void { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -278,7 +354,7 @@ private function getPlaceOrderMutation(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php index 3bd7ade23ae4b..322d984f5fa75 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/AuthorizenetAcceptjs/Guest/SetPaymentMethodTest.php @@ -113,8 +113,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -171,7 +171,7 @@ private function getPlaceOrderMutation(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php index 1564d00fa5996..7d69c49ae6aa3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php @@ -20,6 +20,7 @@ class CreateBraintreeClientTokenTest extends GraphQlAbstract * @magentoConfigFixture default_store payment/braintree/active 1 * @magentoConfigFixture default_store payment/braintree/environment sandbox * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id + * @magentoConfigFixture default_store payment/braintree/merchant_account_id def_merchant_id * @magentoConfigFixture default_store payment/braintree/public_key def_public_key * @magentoConfigFixture default_store payment/braintree/private_key def_private_key */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php index 84a639af30b0e..ad756dfdd2e44 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Customer/SetPaymentMethodTest.php @@ -8,6 +8,7 @@ namespace Magento\GraphQl\Braintree\Customer; use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Framework\Exception\AuthenticationException; use Magento\Framework\Registry; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; @@ -261,6 +262,34 @@ public function testSetPaymentMethodInvalidMethodInput(string $methodCode) $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap()); } + /** + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store payment/braintree/active 1 + * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1 + * @magentoConfigFixture default_store payment/braintree/environment sandbox + * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id + * @magentoConfigFixture default_store payment/braintree/public_key def_public_key + * @magentoConfigFixture default_store payment/braintree/private_key def_private_key + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @dataProvider dataProviderTestSetPaymentMethodInvalidInput + * @expectedException \Exception + */ + public function testSetPaymentMethodWithoutRequiredPaymentMethodInput() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidPaymentMethodInput($maskedQuoteId); + $this->expectExceptionMessage("for \"braintree\" is missing."); + $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap()); + } + public function dataProviderTestSetPaymentMethodInvalidInput(): array { return [ @@ -273,8 +302,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -371,6 +400,33 @@ private function getSetPaymentBraintreeQueryInvalidInput(string $maskedQuoteId, QUERY; } + /** + * @param string $maskedQuoteId + * @return string + */ + private function getSetPaymentBraintreeQueryInvalidPaymentMethodInput(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input:{ + cart_id:"{$maskedQuoteId}" + payment_method:{ + code:"braintree" + braintree:{ + payment_method_nonce:"fake-valid-nonce" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + /** * @param string $maskedQuoteId * @param string $methodCode @@ -407,7 +463,7 @@ private function getPlaceOrderQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } @@ -437,7 +493,7 @@ private function getPaymentTokenQuery(): string * @param string $username * @param string $password * @return array - * @throws \Magento\Framework\Exception\AuthenticationException + * @throws AuthenticationException */ private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php index 1d48c5253fe84..5ee7dd457657c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php @@ -159,8 +159,8 @@ private function assertPlaceOrderResponse(array $response, string $reservedOrder { self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } private function assertSetPaymentMethodResponse(array $response, string $methodCode): void @@ -259,7 +259,7 @@ private function getPlaceOrderQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php new file mode 100644 index 0000000000000..0e88af2fcb22e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -0,0 +1,476 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test CategoryList GraphQl query + */ +class CategoryListTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterSingleCategoryDataProvider + * @param $field + * @param $condition + * @param $value + */ + public function testFilterSingleCategoryByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : "$value" } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertResponseFields($result['categoryList'][0], $expectedResult); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterMultipleCategoriesDataProvider + * @param $field + * @param $condition + * @param $value + * @param $expectedResult + */ + public function testFilterMultipleCategoriesByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : $value } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(count($expectedResult), $result['categoryList']); + foreach ($expectedResult as $i => $expected) { + $this->assertResponseFields($result['categoryList'][$i], $expected); + } + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryByMultipleFields() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["6","7","8","9","10"]}, name: {match: "Movable"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(3, $result['categoryList']); + + $expectedCategories = [7 => 'Movable', 9 => 'Movable Position 1', 10 => 'Movable Position 2']; + $actualCategories = array_column($result['categoryList'], 'name', 'id'); + $this->assertEquals($expectedCategories, $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterWithInactiveCategory() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "category-2"]}}){ + id + name + url_key + url_path + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $actualCategories = array_column($result['categoryList'], 'url_key', 'id'); + $this->assertContains('category-2', $actualCategories); + $this->assertNotContains('inactive', $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testQueryChildCategoriesWithProducts() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["3"]}}){ + id + name + url_key + url_path + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + url_key + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + } + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $baseCategory = $result['categoryList'][0]; + + $this->assertEquals('Category 1', $baseCategory['name']); + $this->assertArrayHasKey('products', $baseCategory); + //Check base category products + $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); + //Check base category children + $expectedBaseCategoryChildren = [ + ['name' => 'Category 1.1', 'description' => 'Category 1.1 description.'], + ['name' => 'Category 1.2', 'description' => 'Its a description of Test Category 1.2'] + ]; + $this->assertCategoryChildren($baseCategory, $expectedBaseCategoryChildren); + + //Check first child category + $firstChildCategory = $baseCategory['children'][0]; + $this->assertEquals('Category 1.1', $firstChildCategory['name']); + $this->assertEquals('Category 1.1 description.', $firstChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ]; + $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = [['name' =>'Category 1.1.1']]; + $this->assertCategoryChildren($firstChildCategory, $firstChildCategoryChildren); + //Check second child category + $secondChildCategory = $baseCategory['children'][1]; + $this->assertEquals('Category 1.2', $secondChildCategory['name']); + $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = []; + $this->assertCategoryChildren($secondChildCategory, $firstChildCategoryChildren); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testNoResultsFound() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "does-not-exist"]}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * When no filters are supplied, the root category is returned + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testEmptyFiltersReturnRootCategory() + { + $query = <<<QUERY +{ + categoryList{ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $storeRootCategoryId = $storeManager->getStore()->getRootCategoryId(); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals('Default Category', $result['categoryList'][0]['name']); + $this->assertEquals($storeRootCategoryId, $result['categoryList'][0]['id']); + } + + /** + * Filtering with match value less than minimum query should return empty result + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testMinimumMatchQueryLength() + { + $query = <<<QUERY +{ + categoryList(filters: {name: {match: "mo"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * @return array + */ + public function filterSingleCategoryDataProvider(): array + { + return [ + [ + 'ids', + 'eq', + '4', + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'name', + 'match', + 'Movable Position 2', + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ], + [ + 'url_key', + 'eq', + 'category-1-1-1', + [ + 'id' => '5', + 'name' => 'Category 1.1.1', + 'url_key' => 'category-1-1-1', + 'url_path' => 'category-1/category-1-1/category-1-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4/5', + 'position' => '1' + ] + ], + ]; + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function filterMultipleCategoriesDataProvider(): array + { + return[ + //Filter by multiple IDs + [ + 'ids', + 'in', + '["4", "9", "10"]', + [ + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], + //Filter by multiple url keys + [ + 'url_key', + 'in', + '["category-1-2", "movable"]', + [ + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' + ], + [ + 'id' => '13', + 'name' => 'Category 1.2', + 'url_key' => 'category-1-2', + 'url_path' => 'category-1/category-1-2', + 'children_count' => '0', + 'path' => '1/2/3/13', + 'position' => '2' + ] + ] + ], + //Filter by matching multiple names + [ + 'name', + 'match', + '"Position"', + [ + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ], + [ + 'id' => '11', + 'name' => 'Movable Position 3', + 'url_key' => 'movable-position-3', + 'url_path' => 'movable-position-3', + 'children_count' => '0', + 'path' => '1/2/11', + 'position' => '7' + ] + ] + ] + ]; + } + + /** + * Check category products + * + * @param array $category + * @param array $expectedProducts + */ + private function assertCategoryProducts(array $category, array $expectedProducts) + { + $this->assertEquals(count($expectedProducts), $category['products']['total_count']); + $this->assertCount(count($expectedProducts), $category['products']['items']); + $this->assertResponseFields($category['products']['items'], $expectedProducts); + } + + /** + * Check category child categories + * + * @param array $category + * @param array $expectedChildren + */ + private function assertCategoryChildren(array $category, array $expectedChildren) + { + $this->assertArrayHasKey('children', $category); + $this->assertCount(count($expectedChildren), $category['children']); + foreach ($expectedChildren as $i => $expectedChild) { + $this->assertResponseFields($category['children'][$i], $expectedChild); + } + } +} 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 cc0ef7aaf0f5c..8f0260a1b1dae 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -502,6 +502,57 @@ public function testAnchorCategory() $this->assertEquals($expectedResponse, $response); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testBreadCrumbs() + { + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + $categoryCollection->addFieldToFilter('name', 'Category 1.1.1'); + /** @var CategoryInterface $category */ + $category = $categoryCollection->getFirstItem(); + $categoryId = $category->getId(); + $this->assertNotEmpty($categoryId, "Preconditions failed: category is not available."); + $query = <<<QUERY +{ + category(id: {$categoryId}) { + name + breadcrumbs { + category_id + category_name + category_level + category_url_key + category_url_path + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $expectedResponse = [ + 'category' => [ + 'name' => 'Category 1.1.1', + 'breadcrumbs' => [ + [ + 'category_id' => 3, + 'category_name' => "Category 1", + 'category_level' => 2, + 'category_url_key' => "category-1", + 'category_url_path' => "category-1" + ], + [ + 'category_id' => 4, + 'category_name' => "Category 1.1", + 'category_level' => 3, + 'category_url_key' => "category-1-1", + 'category_url_path' => "category-1/category-1-1" + ], + ] + ] + ]; + $this->assertEquals($expectedResponse, $response); + } + /** * @param ProductInterface $product * @param array $actualResponse diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php index e805bc940704a..b6687b4e171d3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php @@ -198,6 +198,6 @@ private function checkImageExists(string $url): bool curl_exec($connection); $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); // phpcs:enable Magento2.Functions.DiscouragedFunction - return $responseStatus === 200 ? true : false; + return $responseStatus === 200; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php index b957292a3ac28..52463485a34f9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php @@ -144,6 +144,6 @@ private function checkImageExists(string $url): bool curl_exec($connection); $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); - return $responseStatus === 200 ? true : false; + return $responseStatus === 200; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php new file mode 100644 index 0000000000000..af237f1bd6fb5 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -0,0 +1,1047 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Api\LinkManagementInterface; +use Magento\ConfigurableProduct\Model\LinkManagement; +use Magento\Customer\Model\Group; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductPriceTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** @var ProductRepositoryInterface $productRepository */ + private $productRepository; + + protected function setUp() :void + { + $this->objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + */ + public function testProductWithSinglePrice() + { + $skus = ['simple']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ]; + + $this->assertPrices($expectedPriceRange, $product['price_range']); + } + + /** + * Pricing for Simple, Grouped and Configurable products with no special or tier prices configured + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_12345.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php + */ + public function testMultipleProductTypes() + { + $skus = ["simple-1", "12345", "grouped"]; + + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(3, $result['products']['items']); + + $expected = [ + "simple-1" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 10 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ], + "12345" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 30 + ], + "final_price" => [ + "value" => 30 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 40 + ], + "final_price" => [ + "value" => 40 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ], + "grouped" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 100 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 100 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertPrices($expected[$product['sku']], $product['price_range']); + } + } + + /** + * Simple products with special price and tier price with % discount + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSimpleProductsWithSpecialPriceAndTierPrice() + { + $skus = ["simple1", "simple2"]; + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + + /** @var $tierPriceExtensionAttributesFactory */ + $tierPriceExtensionAttributesFactory = $this->objectManager->create(ProductTierPriceExtensionFactory::class); + $tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); + + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] + )->setExtensionAttributes($tierPriceExtensionAttribute); + foreach ($skus as $sku) { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get($sku); + $simpleProduct->setTierPrices($tierPrices); + $this->productRepository->save($simpleProduct); + } + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(2, $result['products']['items']); + + $expectedPriceRange = [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 40.1 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 40.1 + ] + ] + ], + "simple2" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 15.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 20.05 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 15.99 + ], + "discount" => [ + "amount_off" => 4.01, + "percent_off" => 20.05 + ] + ] + ] + ]; + $expectedTierPrices = [ + "simple1" => [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] + ], + "simple2" => [ + 0 => [ + 'discount' =>[ + 'amount_off' => 2, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] + + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); + $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); + } + } + + /** + * Check the pricing for a grouped product with simple products having special price set + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + */ + public function testGroupedProductsWithSpecialPriceAndTierPrices() + { + $groupedProductSku = 'grouped'; + $grouped = $this->productRepository->get($groupedProductSku); + //get the associated products + $groupedProductLinks = $grouped->getProductLinks(); + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 87 + ] + ]; + $associatedProductSkus = []; + foreach ($groupedProductLinks as $groupedProductLink) { + $associatedProductSkus[] = $groupedProductLink->getLinkedProductSku(); + } + + foreach ($associatedProductSkus as $associatedProductSku) { + $associatedProduct = $this->productRepository->get($associatedProductSku); + $associatedProduct->setSpecialPrice('95.75'); + $this->productRepository->save($associatedProduct); + $this->saveProductTierPrices($associatedProduct, $tierPriceData); + } + $skus = ['grouped']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 95.75 + ], + "discount" => [ + "amount_off" => 100 - 95.75, + //difference between original and final over original price + "percent_off" => (100 - 95.75)*100/100 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 100 + ], + "final_price" => [ + "value" => 95.75 + ], + "discount" => [ + "amount_off" => 100 - 95.75, + "percent_off" => (100 - 95.75)*100/100 + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertEmpty($product['price_tiers']); + + // update default quantity of each of the associated products to be greater than tier price qty of each of them + foreach ($groupedProductLinks as $groupedProductLink) { + $groupedProductLink->getExtensionAttributes()->setQty(3); + } + $this->productRepository->save($grouped); + $result = $this->graphQlQuery($query); + $product = $result['products']['items'][0]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertEmpty($product['price_tiers']); + } + + /** + * Check pricing for bundled product with one item having special price set and dynamic price turned off + * + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_1.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testBundledProductWithSpecialPriceAndTierPrice() + { + $bundledProductSku = 'bundle-product'; + /** @var Product $bundled */ + $bundled = $this->productRepository->get($bundledProductSku); + $skus = ['bundle-product']; + $bundled->setSpecialPrice(10); + + // set the tier price for the bundled product + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + /** @var $tierPriceExtensionAttributesFactory */ + $tierPriceExtensionAttributesFactory = $this->objectManager->create(ProductTierPriceExtensionFactory::class); + $tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] + )->setExtensionAttributes($tierPriceExtensionAttribute); + $bundled->setTierPrices($tierPrices); + // Set Price view to PRICE RANGE + $bundled->setPriceView(0); + $this->productRepository->save($bundled); + + //Bundled product with dynamic prices turned OFF + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $bundleRegularPrice = 10; + $firstOptionPrice = 2.75; + $secondOptionPrice = 6.75; + + $minRegularPrice = $bundleRegularPrice + $firstOptionPrice ; + //Apply special price of 10% on minRegular price + $minFinalPrice = round($minRegularPrice * 0.1, 2); + + $maxRegularPrice = $bundleRegularPrice + $secondOptionPrice; + $maxFinalPrice = round($maxRegularPrice* 0.1, 2); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $minRegularPrice + ], + "final_price" => [ + "value" => $minFinalPrice + ], + "discount" => [ + "amount_off" => $minRegularPrice - $minFinalPrice, + "percent_off" => round(($minRegularPrice - $minFinalPrice)*100/$minRegularPrice, 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $maxRegularPrice + ], + "final_price" => [ + "value" => $maxFinalPrice + ], + "discount" => [ + "amount_off" => $maxRegularPrice - $maxFinalPrice, + "percent_off" => round(($maxRegularPrice - $maxFinalPrice)*100/$maxRegularPrice, 2) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] + ] + ); + } + + /** + * Check pricing for bundled product with spl price, tier price with dynamic price turned on + * + * @magentoApiDataFixture Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php + */ + public function testBundledWithSpecialPriceAndTierPriceWithDynamicPrice() + { + $skus = ['bundle-product']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $minRegularPrice = 10; + $maxRegularPrice = 20; + + //Apply 10% special price on the cheapest simple product in bundle + $minFinalPrice = round(5.99 * 0.1, 2); + //Apply 10% special price on the expensive product in bundle + $maxFinalPrice = round(15.99 * 0.1, 2); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $minRegularPrice + ], + "final_price" => [ + "value" => $minFinalPrice + ], + "discount" => [ + "amount_off" => $minRegularPrice - $minFinalPrice, + "percent_off" => round(($minRegularPrice - $minFinalPrice)*100/$minRegularPrice, 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $maxRegularPrice + ], + "final_price" => [ + "value" => $maxFinalPrice + ], + "discount" => [ + "amount_off" => $maxRegularPrice - $maxFinalPrice, + "percent_off" => round(($maxRegularPrice - $maxFinalPrice)*100/$maxRegularPrice, 2) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => 1, + 'percent_off' => 10 + ], + 'final_price' =>['value'=> 0], + 'quantity' => 2 + ] + ] + ); + } + + /** + * Check pricing for Configurable product with each variants having special price and tier prices + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_12345.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConfigurableProductWithVariantsHavingSpecialAndTierPrices() + { + $configurableProductSku ='12345'; + /** @var LinkManagementInterface $configurableProductLink */ + $configurableProductLinks = $this->objectManager->get(LinkManagement::class); + $configurableProductVariants = $configurableProductLinks->getChildren($configurableProductSku); + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 20 + ] + ]; + foreach ($configurableProductVariants as $configurableProductVariant) { + $configurableProductVariant->setSpecialPrice('25.99'); + $this->productRepository->save($configurableProductVariant); + $this->saveProductTierPrices($configurableProductVariant, $tierPriceData); + } + $sku = ['12345']; + $query = $this->getQueryConfigurableProductAndVariants($sku); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $regularPrice = []; + $finalPrice = []; + foreach ($configurableProductVariants as $configurableProductVariant) { + $regularPrice[] = $configurableProductVariant->getPrice(); + $finalPrice[] = $configurableProductVariant->getSpecialPrice(); + } + $regularPriceCheapestVariant = 30; + $specialPrice = 25.99; + $regularPriceExpensiveVariant = 40; + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => $regularPriceCheapestVariant + ], + "final_price" => [ + "value" => $specialPrice + ], + "discount" => [ + "amount_off" => $regularPriceCheapestVariant - $specialPrice, + "percent_off" => round( + ($regularPriceCheapestVariant - $specialPrice)*100/$regularPriceCheapestVariant, + 2 + ) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $regularPriceExpensiveVariant + ], + "final_price" => [ + "value" => $specialPrice + ], + "discount" => [ + "amount_off" => $regularPriceExpensiveVariant - $specialPrice, + "percent_off" => round( + ($regularPriceExpensiveVariant - $specialPrice)*100/$regularPriceExpensiveVariant, + 2 + ) + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + //configurable product's tier price is empty + $this->assertEmpty($product['price_tiers']); + $this->assertCount(2, $product['variants']); + + $configurableVariantsInResponse = array_map(null, $product['variants'], $configurableProductVariants); + + foreach ($configurableVariantsInResponse as $key => $configurableVariantPriceData) { + //validate that the tier prices and price range for each configurable variants are not empty + $this->assertNotEmpty($configurableVariantPriceData[0]['product']['price_range']); + $this->assertNotEmpty($configurableVariantPriceData[0]['product']['price_tiers']); + $this->assertResponseFields( + $configurableVariantsInResponse[$key][0]['product']['price_range'], + [ + "minimum_price" => [ + "regular_price" => [ + "value" => $configurableProductVariants[$key]->getPrice() + ], + "final_price" => [ + "value" => round($configurableProductVariants[$key]->getSpecialPrice(), 2) + ], + "discount" => [ + "amount_off" => ($regularPrice[$key] - $finalPrice[$key]), + "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => $configurableProductVariants[$key]->getPrice() + ], + "final_price" => [ + "value" => round($configurableProductVariants[$key]->getSpecialPrice(), 2) + ], + "discount" => [ + "amount_off" => $regularPrice[$key] - $finalPrice[$key], + "percent_off" => round(($regularPrice[$key] - $finalPrice[$key])*100/$regularPrice[$key], 2) + ] + ] + ] + ); + + $this->assertResponseFields( + $configurableVariantsInResponse[$key][0]['product']['price_tiers'], + [ + 0 => [ + 'discount' =>[ + 'amount_off' => $regularPrice[$key] - $tierPriceData[0]['value'], + 'percent_off' => round( + ( + $regularPrice[$key] - $tierPriceData[0]['value'] + ) * 100/$regularPrice[$key], + 2 + ) + ], + 'final_price' =>['value'=> $tierPriceData[0]['value']], + 'quantity' => 2 + ] + ] + ); + } + } + + /** + * Check the pricing for downloadable product type + * + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable.php + */ + public function testDownloadableProductWithSpecialPriceAndTierPrices() + { + $downloadableProductSku = 'downloadable-product'; + /** @var Product $downloadableProduct */ + $downloadableProduct = $this->productRepository->get($downloadableProductSku); + //setting the special price for the product + $downloadableProduct->setSpecialPrice('5.75'); + $this->productRepository->save($downloadableProduct); + //setting the tier price data for the product + $tierPriceData = [ + [ + 'customer_group_id' => Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 7 + ] + ]; + $this->saveProductTierPrices($downloadableProduct, $tierPriceData); + $skus = ['downloadable-product']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.75 + ], + "discount" => [ + "amount_off" => 4.25, + //discount amount over regular price value + "percent_off" => (4.25/10)*100 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 5.75 + ], + "discount" => [ + "amount_off" => 4.25, + "percent_off" => (4.25/10)*100 + ] + ] + ]; + $this->assertPrices($expectedPriceRange, $product['price_range']); + $this->assertResponseFields( + $product['price_tiers'], + [ + 0 => [ + 'discount' =>[ + //regualr price - tier price value + 'amount_off' => 3, + 'percent_off' => 30 + ], + 'final_price' =>['value'=> 7], + 'quantity' => 2 + ] + ] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/CatalogRule/_files/catalog_rule_10_off_not_logged.php + */ + public function testProductWithCatalogDiscount() + { + $skus = ["virtual-product", "configurable"]; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(2, $result['products']['items']); + + $expected = [ + "virtual-product" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ] + ], + "configurable" => [ + "minimum_price" => [ + "regular_price" => [ + "value" => 10 + ], + "final_price" => [ + "value" => 9 + ], + "discount" => [ + "amount_off" => 1, + "percent_off" => 10 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 18 + ], + "discount" => [ + "amount_off" => 2, + "percent_off" => 10 + ] + ] + ] + ]; + + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertPrices($expected[$product['sku']], $product['price_range']); + } + } + + /** + * Get GraphQl query to fetch products by sku + * + * @param array $skus + * @return string + */ + private function getProductQuery(array $skus): string + { + $stringSkus = '"' . implode('","', $skus) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSkus]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{ + value + } + quantity + } + } + } +} +QUERY; + } + + /** + * Get GraphQl query to fetch Configurable product and its variants by sku + * + * @param array $sku + * @return string + */ + private function getQueryConfigurableProductAndVariants(array $sku): string + { + $stringSku = '"' . implode('","', $sku) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSku]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price {regular_price + { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{value} + quantity + } + ... on ConfigurableProduct{ + variants{ + product{ + + sku + price_range { + minimum_price {regular_price {value} + final_price { + value + + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + + } + final_price { + value + + } + discount { + amount_off + percent_off + } + } + } + price_tiers{ + discount{ + amount_off + percent_off + } + final_price{value} + quantity + } + + } + } + } + } + } + } + +QUERY; + } + + /** + * Check prices from graphql response + * + * @param $expectedPrices + * @param $actualPrices + * @param string $currency + */ + private function assertPrices($expectedPrices, $actualPrices, $currency = 'USD') + { + $priceTypes = ['minimum_price', 'maximum_price']; + + foreach ($priceTypes as $priceType) { + $expected = $expectedPrices[$priceType]; + $actual = $actualPrices[$priceType]; + $this->assertEquals($expected['regular_price']['value'], $actual['regular_price']['value']); + $this->assertEquals( + $expected['regular_price']['currency'] ?? $currency, + $actual['regular_price']['currency'] + ); + $this->assertEquals($expected['final_price']['value'], $actual['final_price']['value']); + $this->assertEquals( + $expected['final_price']['currency'] ?? $currency, + $actual['final_price']['currency'] + ); + $this->assertEquals($expected['discount']['amount_off'], $actual['discount']['amount_off']); + $this->assertEquals($expected['discount']['percent_off'], $actual['discount']['percent_off']); + } + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveProductTierPrices(ProductInterface $product, array $tierPriceData) + { + $tierPrices =[]; + $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + /** ProductInterface $product */ + $product->setTierPrices($tierPrices); + $product->save(); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 91f1795935f6a..e1615eb9a667e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -14,7 +14,6 @@ use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; use Magento\Eav\Model\Config; -use Magento\Indexer\Model\Indexer; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Model\Product; @@ -28,6 +27,7 @@ * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessiveClassLength) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ class ProductSearchTest extends GraphQlAbstract { @@ -35,6 +35,7 @@ class ProductSearchTest extends GraphQlAbstract * Verify that layered navigation filters and aggregations are correct for product query * * Filter products by an array of skus + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -84,17 +85,37 @@ public function testFilterLn() $response['products'], 'Filters are missing in product query result.' ); + + $expectedFilters = $this->getExpectedFiltersDataSet(); + $actualFilters = $response['products']['filters']; + // presort expected and actual results as different search engines have different orders + usort($expectedFilters, [$this, 'compareFilterNames']); + usort($actualFilters, [$this, 'compareFilterNames']); + $this->assertFilters( - $response, - $this->getExpectedFiltersDataSet(), + ['products' => ['filters' => $actualFilters]], + $expectedFilters, 'Returned filters data set does not match the expected value' ); } + /** + * Compare arrays by value in 'name' field. + * + * @param array $a + * @param array $b + * @return int + */ + private function compareFilterNames(array $a, array $b) + { + return strcmp($a['name'], $b['name']); + } + /** * Layered navigation for Configurable products with out of stock options * Two configurable products each having two variations and one of the child products of one Configurable set to OOS * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -305,15 +326,17 @@ public function testFilterProductsByDropDownCustomAttribute() /** * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function reIndexAndCleanCache() : void { - $objectManager = Bootstrap::getObjectManager(); - $indexer = $objectManager->create(Indexer::class); - $indexer->load('catalogsearch_fulltext'); - $indexer->reindexAll(); + $appDir = dirname(Bootstrap::getInstance()->getAppTempDir()); + $out = ''; + // phpcs:ignore Magento2.Security.InsecureFunction + exec("php -f {$appDir}/bin/magento indexer:reindex", $out); CacheCleaner::cleanAll(); } + /** * Filter products using an array of multi select custom attributes * @@ -676,9 +699,12 @@ public function testFilterByCategoryIdAndCustomAttribute() 'value'=> '13' ], ]; + // presort expected and actual results as different search engines have different orders + usort($expectedCategoryInAggregrations, [$this, 'compareLabels']); + usort($actualCategoriesFromResponse, [$this, 'compareLabels']); $categoryInAggregations = array_map(null, $expectedCategoryInAggregrations, $actualCategoriesFromResponse); -//Validate the categories and sub-categories data in the filter layer + //Validate the categories and sub-categories data in the filter layer foreach ($categoryInAggregations as $index => $categoryAggregationsData) { $this->assertNotEmpty($categoryAggregationsData); $this->assertEquals( @@ -694,6 +720,18 @@ public function testFilterByCategoryIdAndCustomAttribute() } } + /** + * Compare arrays by value in 'label' field. + * + * @param array $a + * @param array $b + * @return int + */ + private function compareLabels(array $a, array $b) + { + return strcmp($a['label'], $b['label']); + } + /** * Filter by exact match of product url key * @@ -982,6 +1020,7 @@ private function assertFilters($response, $expectedFilters, $message = '') /** * Verify product filtering using price range AND matching skus AND name sorted in DESC order * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1052,13 +1091,14 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc() * expected - error is thrown * Actual - empty array * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSearchWithFilterWithPageSizeEqualTotalCount() { - + $this->reIndexAndCleanCache(); $query = <<<QUERY { @@ -1114,6 +1154,7 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() /** * Filtering for products and sorting using multiple sort parameters * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1486,18 +1527,24 @@ public function testSearchAndSortByRelevance() $this->assertEquals(3, $response['products']['total_count']); $this->assertNotEmpty($response['products']['filters'], 'Filters should have the Category layer'); $this->assertEquals('Colorful Category', $response['products']['filters'][0]['filter_items'][0]['label']); + $this->assertCount(2, $response['products']['aggregations']); $productsInResponse = ['Blue briefs','Navy Blue Striped Shoes','Grey shorts']; + /** @var \Magento\Config\Model\Config $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config::class); + if (strpos($config->getConfigDataValue('catalog/search/engine'), 'elasticsearch') !== false) { + $this->markTestIncomplete('MC-20716'); + } $count = count($response['products']['items']); for ($i = 0; $i < $count; $i++) { $this->assertEquals($productsInResponse[$i], $response['products']['items'][$i]['name']); } - $this->assertCount(2, $response['products']['aggregations']); } /** * Filtering for product with sku "equals" a specific value * If pageSize and current page are not requested, default values are returned * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1748,6 +1795,7 @@ public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() /** * No items are returned if the conditions are not met * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1807,6 +1855,7 @@ public function testQueryFilterNoMatchingItems() /** * Asserts that exception is thrown when current page > totalCount of items returned * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1953,6 +2002,7 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() /** * Verify that invalid current page return an error * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @expectedException \Exception * @expectedExceptionMessage currentPage value must be greater than 0 @@ -1982,6 +2032,7 @@ public function testInvalidCurrentPage() /** * Verify that invalid page size returns an error. * + * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @expectedException \Exception * @expectedExceptionMessage pageSize value must be greater than 0 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 5685fcdb25877..dae71c1767caf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -282,7 +282,6 @@ public function testQueryAllFieldsSimpleProduct() $this->assertBaseFields($product, $response['products']['items'][0]); $this->assertEavAttributes($product, $response['products']['items'][0]); $this->assertOptions($product, $response['products']['items'][0]); - $this->assertTierPrices($product, $response['products']['items'][0]); $this->assertArrayHasKey('websites', $response['products']['items'][0]); $this->assertWebsites($product, $response['products']['items'][0]['websites']); self::assertEquals( @@ -723,24 +722,7 @@ private function assertCustomAttribute($actualResponse) $customAttribute = null; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - - /** - * @param ProductInterface $product - * @param $actualResponse - */ - private function assertTierPrices($product, $actualResponse) - { - $tierPrices = $product->getTierPrices(); - $this->assertNotEmpty($actualResponse['tier_prices'], "Precondition failed: 'tier_prices' must not be empty"); - foreach ($actualResponse['tier_prices'] as $tierPriceIndex => $tierPriceArray) { - foreach ($tierPriceArray as $key => $value) { - /** @var \Magento\Catalog\Model\Product\TierPrice $tierPrice */ - $tierPrice = $tierPrices[$tierPriceIndex]; - $this->assertEquals($value, $tierPrice->getData($key)); - } - } - } - + /** * @param ProductInterface $product * @param $actualResponse diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php index 7a30023c89f7e..0982007daaa44 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php @@ -59,4 +59,50 @@ public function testGetStoreConfig() $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); $this->assertEquals(2, $response['storeConfig']['root_category_id']); } + + /** + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture catalog/seo/product_url_suffix global_test_product_suffix + * @magentoConfigFixture catalog/seo/category_url_suffix global_test_category_suffix + * @magentoConfigFixture catalog/seo/title_separator __ + * @magentoConfigFixture catalog/frontend/list_mode 3 + * @magentoConfigFixture catalog/frontend/grid_per_page_values 16 + * @magentoConfigFixture catalog/frontend/list_per_page_values 8 + * @magentoConfigFixture catalog/frontend/grid_per_page 16 + * @magentoConfigFixture catalog/frontend/list_per_page 8 + * @magentoConfigFixture catalog/frontend/default_sort_by asc + */ + public function testGetStoreConfigGlobal() + { + $query + = <<<QUERY +{ + storeConfig{ + product_url_suffix, + category_url_suffix, + title_separator, + list_mode, + grid_per_page_values, + list_per_page_values, + grid_per_page, + list_per_page, + catalog_default_sort_by, + root_category_id + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('storeConfig', $response); + + $this->assertEquals('global_test_product_suffix', $response['storeConfig']['product_url_suffix']); + $this->assertEquals('global_test_category_suffix', $response['storeConfig']['category_url_suffix']); + $this->assertEquals('__', $response['storeConfig']['title_separator']); + $this->assertEquals('3', $response['storeConfig']['list_mode']); + $this->assertEquals('16', $response['storeConfig']['grid_per_page_values']); + $this->assertEquals(16, $response['storeConfig']['grid_per_page']); + $this->assertEquals('8', $response['storeConfig']['list_per_page_values']); + $this->assertEquals(8, $response['storeConfig']['list_per_page']); + $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); + $this->assertEquals(2, $response['storeConfig']['root_category_id']); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php new file mode 100644 index 0000000000000..95f012f798d02 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForCustomersTest.php @@ -0,0 +1,332 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +class TierPricesForCustomersTest extends GraphQlAbstract +{ + /** @var \Magento\TestFramework\ObjectManager */ + private $objectManager; + + /** @var GetMaskedQuoteIdByReservedOrderId */ + private $getMaskedQuoteIdByReservedOrderId; + + /** @var CustomerTokenServiceInterface */ + private $customerTokenService; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForGeneralGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData =[ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForGeneralAndAllCustomerGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 7 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 7 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(2, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForNotLoggedInGroupOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertEmpty($response['products']['items'][0]['tier_prices']); + + $expectedResponse = []; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForNotLoggedInAndGeneralGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 7, + 'value'=> 6.5 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForAllCustomerGroupsOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ], + + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testTierPricesForAllCustomerGroupsAndNotLoggedInGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ], + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + // Query response with headers for customers + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveTierPrices($product, $tierPriceData) + { + $tierPrices = []; + /** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ + $tierPriceFactory = $this->objectManager + ->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + } + $product->setTierPrices($tierPrices); + $product->save(); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + name + tier_prices { + customer_group_id + percentage_value + qty + value + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + $headerMap = [ 'Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php new file mode 100644 index 0000000000000..d4c834c0aea6a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/TierPricesForGuestsTest.php @@ -0,0 +1,301 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; + +class TierPricesForGuestsTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForGeneralGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData =[ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + + $expectedResponse = []; + $this->assertEmpty($response['products']['items'][0]['tier_prices']); + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForGeneralAndAllCustomerGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 6 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForNotLoggedInGroupOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForNotLoggedInAndGeneralGroups() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 6.5 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 6 + ] + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 6.5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForAllCustomerGroupsOnly() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ], + + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 6, + 'value'=> 5 + ] + ]; + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(1, $response['products']['items'][0]['tier_prices']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testTierPricesForAllCustomerGroupsAndNotLoggedInGroup() + { + $productSku = 'simple'; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + $tierPriceData = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ], + ]; + + $this->saveTierPrices($product, $tierPriceData); + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['tier_prices']); + $this->assertArrayHasKey('tier_prices', $response['products']['items'][0]); + $expectedResponse = [ + [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 8 + ], + [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'percentage_value'=> null, + 'qty'=> 4, + 'value'=> 6.5 + ] + ]; + + $this->assertResponseFields($response['products']['items'][0]['tier_prices'], $expectedResponse); + $this->assertCount(2, $response['products']['items'][0]['tier_prices']); + } + + /** + * @param ProductInterface $product + * @param array $tierPriceData + */ + private function saveTierPrices($product, $tierPriceData) + { + $tierPrices = []; + /** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ + $tierPriceFactory = $this->objectManager + ->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + foreach ($tierPriceData as $tierPrice) { + $tierPrices[] = $tierPriceFactory->create( + [ + 'data' => $tierPrice + ] + ); + } + $product->setTierPrices($tierPrices); + $product->save(); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + name + tier_prices { + customer_group_id + percentage_value + qty + value + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php new file mode 100644 index 0000000000000..29696e29908fe --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -0,0 +1,499 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogUrlRewrite; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlRewrite; + +/** + * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. + */ +class UrlResolverTest extends GraphQlAbstract +{ + /** @var ObjectManager */ + private $objectManager; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * Tests if target_path(relative_url) is resolved for Product entity + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlResolver() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + $redirectCode = $actualUrls->getRedirectType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + $redirectCode + ); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlWithNonSeoFriendlyUrlInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + // even of non seo friendly path requested, the seo friendly path should be prefered + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + $nonSeoFriendlyPath = $actualUrls->getTargetPath(); + $redirectCode = $actualUrls->getRedirectType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $nonSeoFriendlyPath, + $relativePath, + $expectedType, + $redirectCode + ); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testRedirectsAndCustomInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + // generate permanent redirects + $renamedKey = 'p002-ren'; + $product->setUrlKey($renamedKey)->setData('save_rewrites_history', true)->save(); + + $storeId = $product->getStoreId(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + $suffix = $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + // querying the end redirect gives the same record + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $renamedKey . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 0 + ); + + // querying a url that's a redirect the active redirected final url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $productSku . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 301 + ); + + // create custom url that doesn't redirect + /** @var UrlRewrite $urlRewriteModel */ + $urlRewriteModel = $this->objectManager->create(UrlRewrite::class); + + $customUrl = 'custom-path'; + $urlRewriteArray = [ + 'entity_type' => 'custom', + 'entity_id' => '0', + 'request_path' => $customUrl, + 'target_path' => 'p002.html', + 'redirect_type' => '0', + 'store_id' => '1', + 'description' => '', + 'is_autogenerated' => '0', + 'metadata' => null, + ]; + foreach ($urlRewriteArray as $key => $value) { + $urlRewriteModel->setData($key, $value); + } + $urlRewriteModel->save(); + + // querying a custom url that should return the target entity but relative should be the custom url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $customUrl, + $actualUrls->getEntityType(), + 0 + ); + + // change custom url that does redirect + $urlRewriteModel->setRedirectType('301'); + $urlRewriteModel->setId($urlRewriteModel->getId()); + $urlRewriteModel->save(); + + ObjectManager::getInstance()->get(\Magento\TestFramework\Helper\CacheCleaner::class)->cleanAll(); + + //modifying query by adding spaces to avoid getting cached values. + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $actualUrls->getRequestPath(), + strtoupper($actualUrls->getEntityType()), + 301 + ); + $urlRewriteModel->delete(); + } + + /** + * Test for category entity + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCategoryUrlResolver() + { + $productSku = 'p002'; + $categoryUrlPath = 'cat-1.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlPath, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $query + = <<<QUERY +{ + category(id:{$categoryId}) { + url_key + url_suffix + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; + + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPath, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Test the use case where the url_key of the existing product is changed + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlRewriteResolver() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + $product->setUrlKey('p002-new')->save(); + + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + + $this->assertEquals($urlPath, 'p002-new' . $response['products']['items'][0]['url_suffix']); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Tests if null is returned when an invalid request_path is provided as input to urlResolver + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testInvalidUrlResolverInput() + { + $productSku = 'p002'; + $urlPath = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + $query + = <<<QUERY +{ + urlResolver(url:"{$urlPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertNull($response['urlResolver']); + } + + /** + * Test for category entity with leading slash + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCategoryUrlWithLeadingSlash() + { + $productSku = 'p002'; + $categoryUrlPath = 'cat-1.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlPath, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $relativePath = $actualUrls->getRequestPath(); + $expectedType = $actualUrls->getEntityType(); + + $query + = <<<QUERY +{ + category(id:{$categoryId}) { + url_key + url_suffix + } +} +QUERY; + $response = $this->graphQlQuery($query); + $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; + $urlPathWithLeadingSlash = "/{$urlPath}"; + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPathWithLeadingSlash, + $relativePath, + $expectedType, + 0 + ); + } + + /** + * Test for custom type which point to the valid product/category/cms page. + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testGetNonExistentUrlRewrite() + { + $urlPath = 'non-exist-product.html'; + /** @var UrlRewrite $urlRewrite */ + $urlRewrite = $this->objectManager->create(UrlRewrite::class); + $urlRewrite->load($urlPath, 'request_path'); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => 1 + ] + ); + $relativePath = $actualUrls->getRequestPath(); + + $query = <<<QUERY +{ + urlResolver(url:"{$urlPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals('PRODUCT', $response['urlResolver']['type']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * Assert response from GraphQl + * + * @param string $productId + * @param string $urlKey + * @param string $relativePath + * @param string $expectedType + * @param int $redirectCode + */ + private function queryUrlAndAssertResponse( + int $productId, + string $urlKey, + string $relativePath, + string $expectedType, + int $redirectCode + ): void { + $query + = <<<QUERY +{ + urlResolver(url:"{$urlKey}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($productId, $response['urlResolver']['id']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $this->assertEquals($redirectCode, $response['urlResolver']['redirectCode']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php new file mode 100644 index 0000000000000..072c6bc38de70 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CmsUrlRewrite; + +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Cms\Helper\Page as PageHelper; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. + */ +class UrlResolverTest extends GraphQlAbstract +{ + /** @var ObjectManager */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCMSPageUrlResolver() + { + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $cmsPageId = $page->getId(); + $requestPath = $page->getIdentifier(); + + /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ + $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); + + /** @param \Magento\Cms\Api\Data\PageInterface $page */ + $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); + $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; + + $query + = <<<QUERY +{ + urlResolver(url:"{$requestPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + + // querying by non seo friendly url path should return seo friendly relative url + $query + = <<<QUERY +{ + urlResolver(url:"{$targetPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * Test resolution of '/' path to home page + */ + public function testResolveSlash() + { + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ + $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); + $homePageIdentifier = $scopeConfigInterface->getValue( + PageHelper::XML_PATH_HOME_PAGE, + ScopeInterface::SCOPE_STORE + ); + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load($homePageIdentifier); + $homePageId = $page->getId(); + $query + = <<<QUERY +{ + urlResolver(url:"/") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); + $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 378d3e7dcd9aa..39b69f86cbe13 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -7,6 +7,7 @@ namespace Magento\GraphQl\ConfigurableProduct; +use Exception; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -139,6 +140,62 @@ public function testAddMultipleConfigurableProductToCart() } } + /** + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find specified product. + */ + public function testAddVariationFromAnotherConfigurableProductWithTheSameSuperAttributeToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable_12345')); + $product = current($searchResponse['products']['items']); + + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + + $sku = 'simple_20'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + $quantity + ); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find specified product. + */ + public function testAddVariationFromAnotherConfigurableProductWithDifferentSuperAttributeToCart() + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable_12345')); + $product = current($searchResponse['products']['items']); + + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = $product['sku']; + + $sku = 'simple_20'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $sku, + $quantity + ); + + $this->graphQlMutation($query); + } + /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php new file mode 100644 index 0000000000000..4f4e7ecab6fe3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Exception\NoSuchEntityException as NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * checks that qty of configurable product is updated in cart + */ +class UpdateConfigurableCartItemsTest extends GraphQlAbstract +{ + /** + * @var QuoteIdMaskFactory + */ + protected $quoteIdMaskFactory; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php + */ + public function testUpdateConfigurableCartItemQuantity() + { + $reservedOrderId = 'test_cart_with_configurable'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $productSku = 'simple_10'; + $newQuantity = 123; + $quoteItem = $this->getQuoteItemBySku($productSku, $reservedOrderId); + + $query = $this->getQuery($maskedQuoteId, (int)$quoteItem->getId(), $newQuantity); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('updateCartItems', $response); + self::assertArrayHasKey('quantity', $response['updateCartItems']['cart']['items']['0']); + self::assertEquals($newQuantity, $response['updateCartItems']['cart']['items']['0']['quantity']); + } + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + } + + /** + * @param string $maskedQuoteId + * @param int $quoteItemId + * @param int $newQuantity + * @return string + */ + private function getQuery(string $maskedQuoteId, int $quoteItemId, int $newQuantity): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id:"$maskedQuoteId" + cart_items: [ + { + cart_item_id: $quoteItemId + quantity: $newQuantity + } + ] + }) { + cart { + items { + quantity + } + } + } +} +QUERY; + } + + /** + * Returns quote item by product SKU + * + * @param string $sku + * @return Item|bool + * @throws NoSuchEntityException + */ + private function getQuoteItemBySku(string $sku, string $reservedOrderId) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $item = false; + foreach ($quote->getAllItems() as $quoteItem) { + if ($quoteItem->getSku() == $sku && $quoteItem->getProductType() == Configurable::TYPE_CODE && + !$quoteItem->getParentItemId()) { + $item = $quoteItem; + break; + } + } + + return $item; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php index 203e9b5cb42e5..04fb304305250 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -43,7 +43,6 @@ protected function setUp() */ public function testCreateCustomerAddress() { - $customerId = 1; $newAddress = [ 'region' => [ 'region' => 'Arizona', @@ -124,11 +123,12 @@ public function testCreateCustomerAddress() $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('createCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['createCustomerAddress']); - $this->assertEquals($customerId, $response['createCustomerAddress']['customer_id']); + $this->assertEquals(null, $response['createCustomerAddress']['customer_id']); $this->assertArrayHasKey('id', $response['createCustomerAddress']); $address = $this->addressRepository->getById($response['createCustomerAddress']['id']); $this->assertEquals($address->getId(), $response['createCustomerAddress']['id']); + $address->setCustomerId(null); $this->assertCustomerAddressesFields($address, $response['createCustomerAddress']); $this->assertCustomerAddressesFields($address, $newAddress); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php index c5714012f38c9..0be968d6d340d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -68,6 +68,7 @@ public function testCreateCustomerAccountWithPassword() QUERY; $response = $this->graphQlMutation($query); + $this->assertEquals(null, $response['createCustomer']['customer']['id']); $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); @@ -113,7 +114,7 @@ public function testCreateCustomerAccountWithoutPassword() /** * @expectedException \Exception - * @expectedExceptionMessage "input" value should be specified + * @expectedExceptionMessage Field CustomerInput.email of required type String! was not provided */ public function testCreateCustomerIfInputDataIsEmpty() { @@ -139,7 +140,7 @@ public function testCreateCustomerIfInputDataIsEmpty() /** * @expectedException \Exception - * @expectedExceptionMessage Required parameters are missing: Email + * @expectedExceptionMessage Field CustomerInput.email of required type String! was not provided */ public function testCreateCustomerIfEmailMissed() { @@ -274,7 +275,7 @@ public function testCreateCustomerIfNameEmpty() QUERY; $this->graphQlMutation($query); } - + /** * @magentoConfigFixture default_store newsletter/general/active 0 */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php index e0c6841b2ea2b..c1573d7dbd8af 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php @@ -62,7 +62,7 @@ public function testGetCustomerWithAddresses() is_array([$response['customer']['addresses']]), " Addresses field must be of an array type." ); - self::assertEquals($customer->getId(), $response['customer']['id']); + self::assertEquals(null, $response['customer']['id']); $this->assertCustomerAddressesFields($customer, $response); } @@ -105,7 +105,7 @@ public function testGetCustomerAddressIfUserIsNotAuthorized() * @param CustomerInterface $customer * @param array $actualResponse */ - public function assertCustomerAddressesFields($customer, $actualResponse) + private function assertCustomerAddressesFields($customer, $actualResponse) { /** @var AddressInterface $addresses */ $addresses = $customer->getAddresses(); @@ -113,7 +113,7 @@ public function assertCustomerAddressesFields($customer, $actualResponse) $this->assertNotEmpty($addressValue); $assertionMap = [ ['response_field' => 'id', 'expected_value' => $addresses[$addressKey]->getId()], - ['response_field' => 'customer_id', 'expected_value' => $addresses[$addressKey]->getCustomerId()], + ['response_field' => 'customer_id', 'expected_value' => 0], ['response_field' => 'region_id', 'expected_value' => $addresses[$addressKey]->getRegionId()], ['response_field' => 'country_id', 'expected_value' => $addresses[$addressKey]->getCountryId()], ['response_field' => 'telephone', 'expected_value' => $addresses[$addressKey]->getTelephone()], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php index 928a263e8531b..b15a799ae7521 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php @@ -50,6 +50,7 @@ public function testGetCustomer() $query = <<<QUERY query { customer { + id firstname lastname email @@ -58,6 +59,7 @@ public function testGetCustomer() QUERY; $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->assertEquals(null, $response['customer']['id']); $this->assertEquals('John', $response['customer']['firstname']); $this->assertEquals('Smith', $response['customer']['lastname']); $this->assertEquals($currentEmail, $response['customer']['email']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php index 9840236dc9896..625d027f58d24 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -59,7 +59,6 @@ public function testUpdateCustomerAddress() { $userName = 'customer@example.com'; $password = 'password'; - $customerId = 1; $addressId = 1; $mutation = $this->getMutation($addressId); @@ -67,7 +66,7 @@ public function testUpdateCustomerAddress() $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); $this->assertArrayHasKey('updateCustomerAddress', $response); $this->assertArrayHasKey('customer_id', $response['updateCustomerAddress']); - $this->assertEquals($customerId, $response['updateCustomerAddress']['customer_id']); + $this->assertEquals(null, $response['updateCustomerAddress']['customer_id']); $this->assertArrayHasKey('id', $response['updateCustomerAddress']); $address = $this->addressRepository->getById($addressId); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index 178d10b3c35a4..d1c6638e8d5ff 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -69,7 +69,7 @@ public function testUpdateCustomer() middlename: "{$newMiddlename}" lastname: "{$newLastname}" suffix: "{$newSuffix}" - dob: "{$newDob}" + date_of_birth: "{$newDob}" taxvat: "{$newTaxVat}" email: "{$newEmail}" password: "{$currentPassword}" @@ -82,7 +82,7 @@ public function testUpdateCustomer() middlename lastname suffix - dob + date_of_birth taxvat email gender @@ -102,7 +102,7 @@ public function testUpdateCustomer() $this->assertEquals($newMiddlename, $response['updateCustomer']['customer']['middlename']); $this->assertEquals($newLastname, $response['updateCustomer']['customer']['lastname']); $this->assertEquals($newSuffix, $response['updateCustomer']['customer']['suffix']); - $this->assertEquals($newDob, $response['updateCustomer']['customer']['dob']); + $this->assertEquals($newDob, $response['updateCustomer']['customer']['date_of_birth']); $this->assertEquals($newTaxVat, $response['updateCustomer']['customer']['taxvat']); $this->assertEquals($newEmail, $response['updateCustomer']['customer']['email']); $this->assertEquals($newGender, $response['updateCustomer']['customer']['gender']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php index 6b8aad83edac7..d0ad772e9bb27 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CustomerDownloadableProduct/CustomerDownloadableProductTest.php @@ -63,6 +63,18 @@ public function testGuestCannotAccessDownloadableProducts() { $this->graphQlQuery($this->getQuery()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_download_limit.php + * @magentoApiDataFixture Magento/Downloadable/_files/customer_order_with_downloadable_product.php + */ + public function testRemainingDownloads() + { + $query = $this->getQuery(); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('remaining_downloads', $response['customerDownloadableProducts']['items'][0]); + self::assertEquals(100, $response['customerDownloadableProducts']['items'][0]['remaining_downloads']); + } /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php new file mode 100644 index 0000000000000..2588de97bad7d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php @@ -0,0 +1,549 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; + +/** + * Test cases for applying cart promotions to items in cart + */ +class CartPromotionsTest extends GraphQlAbstract +{ + /** + * Test adding single cart rule to multiple products in a cart + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + */ + public function testCartPromotionSingleCartRule() + { + $skus =['simple1', 'simple2']; + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels = $rule->getStoreLabels(); + } + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInCart = [$prod1, $prod2]; + //validating the line item prices, quantity and discount + $this->assertLineItemDiscountPrices($response, $productsInCart, $qty, $ruleLabels); + //total discount on the cart which is the sum of the individual row discounts + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 21.98); + } + + /** + * Assert the row total discounts and individual discount break down and cart rule labels + * + * @param $response + * @param $productsInCart + * @param $qty + * @param $ruleLabels + */ + private function assertLineItemDiscountPrices($response, $productsInCart, $qty, $ruleLabels) + { + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'total_item_discount' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5], + 'label' => $ruleLabels[0] + ] + ] + ], + ] + ); + } + } + + /** + * Apply multiple cart rules to multiple products in a cart + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items.php + */ + public function testCartPromotionsMultipleCartRules() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + $skus =['simple1', 'simple2']; + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels[] = $rule->getStoreLabels(); + } + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + + //validating the individual discounts per line item and total discounts per line item + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $lineItemDiscount = $productsInResponse[$itemIndex][0]['prices']['discounts']; + $expectedTotalDiscountValue = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5) + + ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5*0.1); + $this->assertEquals( + $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5, + current($lineItemDiscount)['amount']['value'] + ); + $this->assertEquals('TestRule_Label', current($lineItemDiscount)['label']); + + $lineItemDiscountValue = next($lineItemDiscount)['amount']['value']; + $this->assertEquals( + round($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5)*0.1, + $lineItemDiscountValue + ); + $this->assertEquals('10% off with two items_Label', end($lineItemDiscount)['label']); + $actualTotalDiscountValue = $lineItemDiscount[0]['amount']['value']+$lineItemDiscount[1]['amount']['value']; + $this->assertEquals(round($expectedTotalDiscountValue, 2), $actualTotalDiscountValue); + + //removing the elements from the response so that the rest of the response values can be compared + unset($productsInResponse[$itemIndex][0]['prices']['discounts']); + unset($productsInResponse[$itemIndex][0]['prices']['total_item_discount']); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty] + ], + ] + ); + } + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 21.98); + $this->assertEquals($response['cart']['prices']['discounts'][1]['amount']['value'], 2.2); + } + + /** + * Apply cart rules to multiple products in a cart with taxes + * Tax settings : Including and Excluding tax for Price Display and Shopping cart display + * Discount on Prices Includes Tax + * Tax rate = 7.5% + * Cart rule to apply 50% for products assigned to a specific category + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php + */ + public function testCartPromotionsCartRulesWithTaxes() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + $skus =['simple1', 'simple2']; + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + foreach ($productsInCart as $product) { + $product->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product); + } + $categoryId = 66; + /** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ + $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); + foreach ($skus as $sku) { + $categoryLinkManagement->assignProductToCategories( + $sku, + [$categoryId] + ); + } + $qty = 1; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $this->setShippingAddressOnCart($cartId); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $rowTotalIncludingTax = round( + $productsInCart[$itemIndex]->getSpecialPrice()*$qty + + $productsInCart[$itemIndex]->getSpecialPrice()*$qty*.075, + 2 + ); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + // row_total is the line item price without the tax + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + // row_total including tax is the price + price * tax rate + 'row_total_including_tax' => ['value' => $rowTotalIncludingTax], + // discount from cart rule after tax is applied : 50% of row_total_including_tax + 'total_item_discount' => ['value' => round($rowTotalIncludingTax/2, 2)], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => round($rowTotalIncludingTax/2, 2)], + 'label' => 'TestRule_Label' + ] + ] + ], + ] + ); + } + // checking the total discount on the entire cart + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 11.82); + } + + /** + * Apply cart rule with a fixed discount when specific coupon code + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testCartPromotionsWithCoupons() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $prod1 = $productRepository->get('simple1'); + $prod2 = $productRepository->get('simple2'); + $productsInCart = [$prod1, $prod2]; + + $skus =['simple1', 'simple2']; + + /** @var Collection $ruleCollection */ + $ruleCollection = $objectManager->get(Collection::class); + $ruleLabels = []; + /** @var Rule $rule */ + foreach ($ruleCollection as $rule) { + $ruleLabels = $rule->getStoreLabels(); + } + $qty = 2; + // coupon code obtained from the fixture + $couponCode = '2?ds5!2d'; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $this->applyCouponsToCart($cartId, $couponCode); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + $productsInResponse = array_map(null, $response['cart']['items'], $productsInCart); + $count = count($productsInCart); + for ($itemIndex = 0; $itemIndex < $count; $itemIndex++) { + $this->assertNotEmpty($productsInResponse[$itemIndex]); + $sumOfPricesForBothProducts = 43.96; + $rowTotal = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty); + $this->assertResponseFields( + $productsInResponse[$itemIndex][0], + [ + 'quantity' => $qty, + 'prices' => [ + 'row_total' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'row_total_including_tax' => ['value' => $productsInCart[$itemIndex]->getSpecialPrice()*$qty], + 'total_item_discount' => ['value' => round(($rowTotal/$sumOfPricesForBothProducts)*5, 2)], + 'discounts' => [ + 0 =>[ + 'amount' => + ['value' => round(($rowTotal/$sumOfPricesForBothProducts)*5, 2)], + 'label' => $ruleLabels[0] + ] + ] + ], + ] + ); + } + $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 5); + } + + /** + * If no discount is applicable to the cart, row total discount should be zero and no rule label shown + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/buy_3_get_1_free.php + */ + public function testCartPromotionsWhenNoDiscountIsAvailable() + { + $skus =['simple1', 'simple2']; + $qty = 2; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + $this->assertCount(2, $response['cart']['items']); + foreach ($response['cart']['items'] as $cartItems) { + $this->assertEquals(0, $cartItems['prices']['total_item_discount']['value']); + $this->assertNull($cartItems['prices']['discounts']); + } + } + + /** + * Validating if the discount label in the response shows the default value if no label is available on cart rule + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off.php + */ + public function testCartPromotionsWithNoRuleLabels() + { + $skus =['simple1', 'simple2']; + $qty = 1; + $cartId = $this->createEmptyCart(); + $this->addMultipleSimpleProductsToCart($cartId, $qty, $skus[0], $skus[1]); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutation($query); + //total items added to cart + $this->assertCount(2, $response['cart']['items']); + //checking the default label for individual line item when cart rule doesn't have a label set + foreach ($response['cart']['items'] as $cartItem) { + $this->assertEquals('Discount', $cartItem['prices']['discounts'][0]['label']); + } + } + + /** + * Apply coupon to the cart + * + * @param string $cartId + * @param string $couponCode + */ + private function applyCouponsToCart(string $cartId, string $couponCode) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$cartId", coupon_code: "$couponCode"}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + } + + /** + * @param string $cartId + * @return string + */ + private function getCartItemPricesQuery(string $cartId): string + { + return <<<QUERY +{ + cart(cart_id:"{$cartId}"){ + items{ + quantity + prices{ + row_total{ + value + } + row_total_including_tax{ + value + } + total_item_discount{value} + discounts{ + amount{value} + label + } + } + } + prices{ + discounts{ + amount{value} + } + + } + } +} + +QUERY; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query); + $cartId = $response['createEmptyCart']; + return $cartId; + } + + /** + * @param string $cartId + * @param int $sku1 + * @param int $qty + * @param string $sku2 + */ + private function addMultipleSimpleProductsToCart(string $cartId, int $qty, string $sku1, string $sku2): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart(input: { + cart_id: "{$cartId}", + cart_items: [ + { + data: { + quantity: $qty + sku: "$sku1" + } + } + { + data: { + quantity: $qty + sku: "$sku2" + } + } + ] + } + ) { + cart { + items { + product{sku} + quantity + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku1, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertEquals($qty, $response['addSimpleProductsToCart']['cart']['items'][1]['quantity']); + self::assertEquals($sku2, $response['addSimpleProductsToCart']['cart']['items'][1]['product']['sku']); + } + + /** + * Set shipping address for the region for which tax rule is set + * + * @param string $cartId + * @return void + */ + private function setShippingAddressOnCart(string $cartId) :void + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "John" + lastname: "Doe" + company: "Magento" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36043" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + region{label} + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertEquals( + 'Montgomery', + $response['setShippingAddressesOnCart']['cart']['shipping_addresses'][0]['city'] + ); + self::assertEquals( + 'Alabama', + $response['setShippingAddressesOnCart']['cart']['shipping_addresses'][0]['region']['label'] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php index 6a06b143d5fcf..ddf94fbcc1edf 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -259,7 +259,6 @@ private function setBillingAddress(string $cartId): void telephone: "88776655" region: "TX" country_code: "US" - save_in_address_book: false } } } @@ -298,7 +297,6 @@ private function setShippingAddress(string $cartId): array postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -426,7 +424,7 @@ private function placeOrder(string $cartId): string } ) { order { - order_id + order_number } } } @@ -434,10 +432,10 @@ private function placeOrder(string $cartId): string $response = $this->graphQlMutation($query, [], '', $this->headers); self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertNotEmpty($response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_number']); - return $response['placeOrder']['order']['order_id']; + return $response['placeOrder']['order']['order_number']; } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php index cb471d8f0f936..38d9ddc4fecc1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php @@ -86,8 +86,8 @@ public function testPlaceOrder() $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('placeOrder', $response); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } /** @@ -114,7 +114,7 @@ public function testPlaceOrderIfCartIdIsMissed() mutation { placeOrder(input: {}) { order { - order_id + order_number } } } @@ -313,7 +313,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php index ec4ab012d37dc..2e4fa0a4cdc96 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -7,6 +7,9 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; @@ -45,6 +48,19 @@ class SetBillingAddressOnCartTest extends GraphQlAbstract */ private $customerTokenService; + /** + * @var AddressRepositoryInterface + */ + private $customerAddressRepository; + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + protected function setUp() { $objectManager = Bootstrap::getObjectManager(); @@ -53,6 +69,9 @@ protected function setUp() $this->quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerAddressRepository = $objectManager->get(AddressRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); } /** @@ -81,7 +100,6 @@ public function testSetNewBillingAddress() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -140,7 +158,6 @@ public function testSetNewBillingAddressWithSameAsShippingParameter() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } same_as_shipping: true } @@ -239,6 +256,42 @@ public function testSetBillingAddressFromAddressBook() $this->assertSavedBillingAddressFields($billingAddressResponse); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testVerifyBillingAddressType() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $billingAddress = $response['setBillingAddressOnCart']['cart']['billing_address']; + self::assertArrayHasKey('__typename', $billingAddress); + self::assertEquals('BillingCartAddress', $billingAddress['__typename']); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php @@ -301,7 +354,6 @@ public function testSetNewBillingAddressAndFromAddressBookAtSameTime() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -383,7 +435,6 @@ public function testSetNewBillingAddressWithSameAsShippingAndMultishipping() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } same_as_shipping: true } @@ -620,7 +671,6 @@ public function testSetNewBillingAddressWithRedundantStreetLine() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -663,7 +713,6 @@ public function testSetBillingAddressWithLowerCaseCountry() postcode: "887766" country_code: "us" telephone: "88776655" - save_in_address_book: false } } } @@ -696,6 +745,140 @@ public function testSetBillingAddressWithLowerCaseCountry() $this->assertNewAddressFields($billingAddressResponse); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: true + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(1, $addresses); + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithNotSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(0, $addresses); + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 192c10a67aa6b..aff124c522309 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -85,7 +85,71 @@ public function testSetPaymentOnCartWithSimpleProduct() self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php + * + * @dataProvider dataProviderSetPaymentOnCartWithException + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetPaymentOnCartWithException(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setPaymentMethodAndPlaceOrder( + input: { + {$input} + } + ) { + order { + order_number + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderSetPaymentOnCartWithException(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: { + code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '" + }', + 'Required parameter "cart_id" is missing', + ], + 'missed_payment_method' => [ + 'cart_id: "cart_id_value"', + 'Required parameter "code" for "payment_method" is missing.', + ], + 'place_order_with_out_of_stock_products' => [ + 'cart_id: "cart_id_value" + payment_method: { + code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '" + }', + 'Unable to place order: Some of the products are out of stock.', + ], + ]; } /** @@ -128,7 +192,7 @@ public function testSetPaymentOnCartWithVirtualProduct() self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); } /** @@ -254,7 +318,7 @@ private function getQuery( } ) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index e74b7c41b3983..42b662d264a91 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -7,6 +7,9 @@ namespace Magento\GraphQl\Quote\Customer; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Model\QuoteFactory; @@ -45,6 +48,21 @@ class SetShippingAddressOnCartTest extends GraphQlAbstract */ private $customerTokenService; + /** + * @var AddressRepositoryInterface + */ + private $customerAddressRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + protected function setUp() { $objectManager = Bootstrap::getObjectManager(); @@ -53,6 +71,9 @@ protected function setUp() $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerAddressRepository = $objectManager->get(AddressRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); } /** @@ -82,7 +103,6 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } customer_notes: "Test note" } @@ -148,7 +168,6 @@ public function testSetNewShippingAddressOnCartWithVirtualProduct() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -213,6 +232,44 @@ public function testSetShippingAddressFromAddressBook() /** * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testVerifyShippingAddressType() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $shippingAddresses = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('__typename', $shippingAddresses); + self::assertEquals('ShippingCartAddress', $shippingAddresses['__typename']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php @@ -276,7 +333,6 @@ public function testSetNewShippingAddressAndFromAddressBookAtSameTime() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -458,7 +514,6 @@ public function testSetMultipleNewShippingAddresses() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } }, { @@ -472,7 +527,6 @@ public function testSetMultipleNewShippingAddresses() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -516,7 +570,6 @@ public function testSetNewShippingAddressOnCartWithRedundantStreetLine() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -657,6 +710,148 @@ public function testSetShippingAddressWithLowerCaseCountry() $this->assertEquals('CA', $address['region']['code']); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressWithSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: true + } + customer_notes: "Test note" + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + customer_notes + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(1, $addresses); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressWithNotSaveInAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + customer_notes: "Test note" + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + customer_notes + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $customer = $this->customerRepository->get('customer@example.com'); + $searchCriteria = $this->searchCriteriaBuilder->addFilter('parent_id', $customer->getId())->create(); + $addresses = $this->customerAddressRepository->getList($searchCriteria)->getItems(); + + self::assertCount(0, $addresses); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + + foreach ($addresses as $address) { + $this->customerAddressRepository->delete($address); + } + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php index 16f291be91078..90ebec763b227 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AllowGuestCheckoutOptionTest.php @@ -108,7 +108,6 @@ public function testSetBillingAddressToGuestCustomerCart() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -221,7 +220,6 @@ public function testSetNewShippingAddressOnCartWithGuestCheckoutDisabled() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -318,7 +316,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php new file mode 100644 index 0000000000000..9adafa7e097f2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php @@ -0,0 +1,192 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupons to Cart functionality for guest + */ +class ApplyCouponsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponsToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponsToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponsToCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponsToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponsWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @expectedException \Exception + */ + public function testApplyCouponsWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Required parameter "cart_id" is missing' + ], + 'missed_coupon_code' => [ + 'cart_id: "test_quote"', + 'Required parameter "coupon_code" is missing' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupons { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php index 95308a350c953..315c046148506 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -219,7 +219,6 @@ private function setBillingAddress(string $cartId): void telephone: "88776655" region: "TX" country_code: "US" - save_in_address_book: false } } } @@ -258,7 +257,6 @@ private function setShippingAddress(string $cartId): array postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -386,7 +384,7 @@ private function placeOrder(string $cartId): void } ) { order { - order_id + order_number } } } @@ -394,8 +392,8 @@ private function placeOrder(string $cartId): void $response = $this->graphQlMutation($query); self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order', $response['placeOrder']); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertNotEmpty($response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_number']); } public function tearDown() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php index 6a75cab1ff4c3..a6e4a4afa9825 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php @@ -133,12 +133,7 @@ public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() $shippingAddress = current($response['cart']['shipping_addresses']); self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); - self::assertNull($shippingAddress['selected_shipping_method']['method_code']); - self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); - self::assertNull($shippingAddress['selected_shipping_method']['method_title']); - self::assertNull($shippingAddress['selected_shipping_method']['amount']); + self::assertNull($shippingAddress['selected_shipping_method']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 2dc5b53b31c7a..52caf836d3b46 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -79,8 +79,8 @@ public function testPlaceOrder() $response = $this->graphQlMutation($query); self::assertArrayHasKey('placeOrder', $response); - self::assertArrayHasKey('order_id', $response['placeOrder']['order']); - self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); } /** @@ -105,7 +105,7 @@ public function testPlaceOrderIfCartIdIsMissed() mutation { placeOrder(input: {}) { order { - order_id + order_number } } } @@ -304,7 +304,7 @@ private function getQuery(string $maskedQuoteId): string mutation { placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php index 8e500510494c2..891b4425fe23e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -52,7 +52,6 @@ public function testSetNewBillingAddress() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -110,7 +109,6 @@ public function testSetNewBillingAddressWithSameAsShippingParameter() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } same_as_shipping: true } @@ -186,7 +184,6 @@ public function testSetBillingAddressToCustomerCart() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -263,7 +260,6 @@ public function testSetBillingAddressOnNonExistentCart() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -391,7 +387,6 @@ public function testSetNewBillingAddressWithSameAsShippingAndMultishipping() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } same_as_shipping: true } @@ -437,7 +432,6 @@ public function testSetNewBillingAddressRedundantStreetLine() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } @@ -480,7 +474,6 @@ public function testSetBillingAddressWithLowerCaseCountry() postcode: "887766" country_code: "us" telephone: "88776655" - save_in_address_book: false } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index 50fd9647d7c54..e38ccf78d420b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -73,7 +73,7 @@ public function testSetPaymentOnCartWithSimpleProduct() self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('order_number', $response['setPaymentMethodAndPlaceOrder']['order']); } /** @@ -111,7 +111,7 @@ public function testSetPaymentOnCartWithVirtualProduct() self::assertArrayHasKey('setPaymentMethodAndPlaceOrder', $response); self::assertArrayHasKey('order', $response['setPaymentMethodAndPlaceOrder']); - self::assertArrayHasKey('order_id', $response['setPaymentMethodAndPlaceOrder']['order']); + self::assertArrayHasKey('order_number', $response['setPaymentMethodAndPlaceOrder']['order']); } /** @@ -232,7 +232,7 @@ private function getQuery( } }) { order { - order_id + order_number } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index 0351a4f58a8e0..2e98773ad9187 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -53,7 +53,6 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } customer_notes: "Test note" } @@ -118,7 +117,6 @@ public function testSetNewShippingAddressOnCartWithVirtualProduct() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -270,7 +268,6 @@ public function testSetNewShippingAddressOnCartWithRedundantStreetLine() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -339,7 +336,6 @@ public function testSetMultipleNewShippingAddresses() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } }, { @@ -353,7 +349,6 @@ public function testSetMultipleNewShippingAddresses() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } ] @@ -393,7 +388,6 @@ public function testSetShippingAddressOnNonExistentCart() postcode: "887766" country_code: "US" telephone: "88776655" - save_in_address_book: false } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php index 9c969befa328b..11a2216b6668f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php @@ -38,9 +38,7 @@ public function testOrdersQuery() query { customerOrders { items { - id - increment_id - created_at + order_number grand_total status } @@ -54,27 +52,27 @@ public function testOrdersQuery() $expectedData = [ [ - 'increment_id' => '100000002', + 'order_number' => '100000002', 'status' => 'processing', 'grand_total' => 120.00 ], [ - 'increment_id' => '100000003', + 'order_number' => '100000003', 'status' => 'processing', 'grand_total' => 130.00 ], [ - 'increment_id' => '100000004', + 'order_number' => '100000004', 'status' => 'closed', 'grand_total' => 140.00 ], [ - 'increment_id' => '100000005', + 'order_number' => '100000005', 'status' => 'complete', 'grand_total' => 150.00 ], [ - 'increment_id' => '100000006', + 'order_number' => '100000006', 'status' => 'complete', 'grand_total' => 160.00 ] @@ -84,19 +82,19 @@ public function testOrdersQuery() foreach ($expectedData as $key => $data) { $this->assertEquals( - $data['increment_id'], - $actualData[$key]['increment_id'], - "increment_id is different than the expected for order - " . $data['increment_id'] + $data['order_number'], + $actualData[$key]['order_number'], + "order_number is different than the expected for order - " . $data['order_number'] ); $this->assertEquals( $data['grand_total'], $actualData[$key]['grand_total'], - "grand_total is different than the expected for order - " . $data['increment_id'] + "grand_total is different than the expected for order - " . $data['order_number'] ); $this->assertEquals( $data['status'], $actualData[$key]['status'], - "status is different than the expected for order - " . $data['increment_id'] + "status is different than the expected for order - " . $data['order_number'] ); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 4657a1e763ae1..076c7bece5ff7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -30,6 +30,7 @@ protected function setUp() /** * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoConfigFixture default_store store/information/name Test Store */ public function testGetStoreConfig() { @@ -62,7 +63,8 @@ public function testGetStoreConfig() secure_base_url, secure_base_link_url, secure_base_static_url, - secure_base_media_url + secure_base_media_url, + store_name } } QUERY; @@ -89,5 +91,6 @@ public function testGetStoreConfig() $response['storeConfig']['secure_base_static_url'] ); $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); + $this->assertEquals('Test Store', $response['storeConfig']['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php index dfbe943ecdcd9..1dc5a813de2b8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php @@ -38,6 +38,12 @@ class ProductViewTest extends GraphQlAbstract /** @var \Magento\Tax\Model\Calculation\Rule[] */ private $fixtureTaxRules; + /** @var string */ + private $defaultRegionSystemSetting; + + /** @var string */ + private $defaultPriceDisplayType; + /** * @var StoreManagerInterface */ @@ -52,19 +58,26 @@ protected function setUp() /** @var \Magento\Config\Model\ResourceModel\Config $config */ $config = $this->objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $this->defaultRegionSystemSetting = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_DEFAULT_REGION + ); + + $this->defaultPriceDisplayType = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE + ); + //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 1 ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 3, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + 3 ); $this->getFixtureTaxRates(); $this->getFixtureTaxRules(); @@ -72,6 +85,9 @@ protected function setUp() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } public function tearDown() @@ -82,16 +98,12 @@ public function tearDown() //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - null, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultRegionSystemSetting ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultPriceDisplayType ); $taxRules = $this->getFixtureTaxRules(); if (count($taxRules)) { @@ -107,6 +119,10 @@ public function tearDown() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } /** @@ -253,7 +269,23 @@ private function getFixtureTaxRules() */ private function assertBaseFields($product, $actualResponse) { - // ['product_object_field_name', 'expected_value'] + $pricesTypes = [ + 'minimalPrice', + 'regularPrice', + 'maximalPrice', + ]; + foreach ($pricesTypes as $priceType) { + if (isset($actualResponse['price'][$priceType]['amount']['value'])) { + $actualResponse['price'][$priceType]['amount']['value'] = + round($actualResponse['price'][$priceType]['amount']['value'], 4); + } + + if (isset($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'])) { + $actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'] = + round($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'], 4); + } + } + // product_object_field_name, expected_value $assertionMap = [ ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], @@ -263,7 +295,7 @@ private function assertBaseFields($product, $actualResponse) [ 'minimalPrice' => [ 'amount' => [ - 'value' => 4.106501, + 'value' => 4.1065, 'currency' => 'USD' ], 'adjustments' => [ @@ -271,7 +303,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', @@ -281,7 +313,7 @@ private function assertBaseFields($product, $actualResponse) ], 'regularPrice' => [ 'amount' => [ - 'value' => 10.750001, + 'value' => 10.7500, 'currency' => 'USD' ], 'adjustments' => [ @@ -289,7 +321,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.750001, + 'value' => 0.7500, 'currency' => 'USD', ], 'code' => 'TAX', @@ -299,7 +331,7 @@ private function assertBaseFields($product, $actualResponse) ], 'maximalPrice' => [ 'amount' => [ - 'value' => 4.106501, + 'value' => 4.1065, 'currency' => 'USD' ], 'adjustments' => [ @@ -307,7 +339,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 8eaf33483531d..5e6415f82b25a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -7,23 +7,15 @@ namespace Magento\GraphQl\UrlRewrite; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\Cms\Helper\Page as PageHelper; -use Magento\Store\Model\ScopeInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\UrlRewrite\Model\UrlRewrite; /** * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. */ class UrlResolverTest extends GraphQlAbstract { - - /** @var ObjectManager */ + /** @var ObjectManager */ private $objectManager; protected function setUp() @@ -31,370 +23,6 @@ protected function setUp() $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); } - /** - * Tests if target_path(relative_url) is resolved for Product entity - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlResolver() - { - $productSku = 'p002'; - $urlPath = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Tests the use case where relative_url is provided as resolver input in the Query - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlWithCanonicalUrlInput() - { - $productSku = 'p002'; - $urlPath = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - $product->getUrlKey(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $canonicalPath = $actualUrls->getTargetPath(); - $query - = <<<QUERY -{ - urlResolver(url:"{$canonicalPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Test for category entity - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testCategoryUrlResolver() - { - $productSku = 'p002'; - $urlPath2 = 'cat-1.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath2, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath2}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * @magentoApiDataFixture Magento/Cms/_files/pages.php - */ - public function testCMSPageUrlResolver() - { - /** @var \Magento\Cms\Model\Page $page */ - $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); - $page->load('page100'); - $cmsPageId = $page->getId(); - $requestPath = $page->getIdentifier(); - - /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ - $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); - - /** @param \Magento\Cms\Api\Data\PageInterface $page */ - $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); - $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; - - $query - = <<<QUERY -{ - urlResolver(url:"{$requestPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); - } - - /** - * Tests the use case where the url_key of the existing product is changed - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testProductUrlRewriteResolver() - { - $productSku = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - $product->setUrlKey('p002-new')->save(); - $urlPath = $product->getUrlKey() . '.html'; - $this->assertEquals($urlPath, 'p002-new.html'); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Tests if null is returned when an invalid request_path is provided as input to urlResolver - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testInvalidUrlResolverInput() - { - $productSku = 'p002'; - $urlPath = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertNull($response['urlResolver']); - } - - /** - * Test for category entity with leading slash - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testCategoryUrlWithLeadingSlash() - { - $productSku = 'p002'; - $urlPath = 'cat-1.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); - $expectedType = $actualUrls->getEntityType(); - - $query = <<<QUERY -{ - urlResolver(url:"/{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); - } - - /** - * Test resolution of '/' path to home page - */ - public function testResolveSlash() - { - /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ - $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); - $homePageIdentifier = $scopeConfigInterface->getValue( - PageHelper::XML_PATH_HOME_PAGE, - ScopeInterface::SCOPE_STORE - ); - /** @var \Magento\Cms\Model\Page $page */ - $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); - $page->load($homePageIdentifier); - $homePageId = $page->getId(); - /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ - $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); - /** @param \Magento\Cms\Api\Data\PageInterface $page */ - $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); - $query - = <<<QUERY -{ - urlResolver(url:"/") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); - } - - /** - * Test for custom type which point to the valid product/category/cms page. - * - * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php - */ - public function testGetNonExistentUrlRewrite() - { - $urlPath = 'non-exist-product.html'; - /** @var UrlRewrite $urlRewrite */ - $urlRewrite = $this->objectManager->create(UrlRewrite::class); - $urlRewrite->load($urlPath, 'request_path'); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $urlPath, - 'store_id' => 1 - ] - ); - $targetPath = $actualUrls->getTargetPath(); - - $query = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals('PRODUCT', $response['urlResolver']['type']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - } - /** * Test for custom type which point to the invalid product/category/cms page. * @@ -411,6 +39,7 @@ public function testNonExistentEntityUrlRewrite() id relative_url type + redirectCode } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php new file mode 100644 index 0000000000000..385e3419bbf6a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php @@ -0,0 +1,733 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; + +/** + * Test for Product Price With FPT + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + */ +class ProductPriceWithFPTTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** @var string[] $objectManager */ + private $initialConfig; + + /** @var ScopeConfigInterface */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + /** @var ScopeConfigInterface $scopeConfig */ + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $currentSettingsArray = [ + 'tax/display/type', + 'tax/weee/enable', + 'tax/weee/display', + 'tax/defaults/region', + 'tax/weee/apply_vat', + 'tax/calculation/price_includes_tax' + ]; + + foreach ($currentSettingsArray as $configPath) { + $this->initialConfig[$configPath] = $this->scopeConfig->getValue( + $configPath + ); + } + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->writeConfig($this->initialConfig); + } + + /** + * Write configuration for weee + * + * @param array $weeTaxSettings + * @return void + */ + private function writeConfig(array $weeTaxSettings): void + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + $this->scopeConfig->clean(); + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxAndIncludeFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxAndIncludeFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceExcludeTaxAndIncludeFPTOnlyProvider settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxAndIncludeFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxAndIncludeFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxAndIncludeFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceExcludeTaxAndIncludeFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxAndIncludeFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $prod2 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + + // final price and regular price are the sum of product price, FPT and product tax + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + } + + /** + * CatalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnly settings data provider + * + * @return array + */ + public function catalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Excluding Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + // final price and regular price are the sum of product price and FPT + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(120.2, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + } + + /** + * CatalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '0', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Excluding Tax + * FPT Display setting: Including FPT and FPT description + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescriptionSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescription(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescription settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescriptionSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '1', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT Only + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnlySettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnly(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + // final price and regular price are the sum of product price and FPT + $this->assertEquals(112.7, $product['price_range']['minimum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['minimum_price']['final_price']['value']); + + $this->assertEquals(112.7, $product['price_range']['maximum_price']['regular_price']['value']); + $this->assertEquals(112.7, $product['price_range']['maximum_price']['final_price']['value']); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals(12.7, $fixedProductTax['amount']['value']); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnly settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnlySettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '0', + ] + ] + ]; + } + + /** + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT Description + * Apply Tax to FPT = Yes + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPTSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPT( + array $weeTaxSettings + ) { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + + //12.7 + 7.5% of 12.7 = 13.65 + $fptWithTax = round(13.65, 2); + // final price and regular price are the sum of product price and FPT + $this->assertEquals(113.65, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertEquals(113.65, round($product['price_range']['minimum_price']['final_price']['value'], 2)); + + $this->assertEquals(113.65, round($product['price_range']['maximum_price']['regular_price']['value'], 2)); + $this->assertEquals(113.65, round($product['price_range']['maximum_price']['final_price']['value'], 2)); + + $this->assertNotEmpty($product['price_range']['minimum_price']['fixed_product_taxes']); + $fixedProductTax = $product['price_range']['minimum_price']['fixed_product_taxes'][0]; + $this->assertEquals($fptWithTax, round($fixedProductTax['amount']['value'], 2)); + $this->assertEquals('fpt_for_all_front_label', $fixedProductTax['label']); + } + + /** + * CatalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPT settings data provider + * + * @return array + */ + public function catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPTSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '0', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Use multiple FPTs per product with the below tax/fpt configurations + * + * Catalog Prices : Including Tax + * Catalog Display setting: Including Tax + * FPT Display setting: Including FPT and FPT description + * Apply tax on FPT : Yes + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_two_fpt.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + */ + public function testCatalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTs(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ + $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); + $taxClassCollection = $taxClassCollectionFactory->create(); + /** @var TaxClassModel $taxClass */ + $taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); + $taxClass = $taxClassCollection->getFirstItem(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setCustomAttribute('tax_class_id', $taxClass->getClassId()); + $product1->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10, 'delete' => '']] + ); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertEquals(124.40, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertCount( + 2, + $product['price_range']['minimum_price']['fixed_product_taxes'], + 'Fixed product tax count is incorrect' + ); + $this->assertResponseFields( + $product['price_range']['minimum_price']['fixed_product_taxes'], + [ + [ + 'amount' => [ + 'value' => 13.6525 + ], + 'label' => 'fpt_for_all_front_label' + ], + [ + 'amount' => [ + 'value' => 10.75 + ], + 'label' => 'fixed_product_attribute_front_label' + ], + ] + ); + } + + /** + * CatalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider settings data provider + * + * @return array + */ + public function catalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/calculation/price_includes_tax' => '1', + 'tax/display/type' => '2', + 'tax/weee/enable' => '1', + 'tax/weee/display' => '1', + 'tax/defaults/region' => '1', + 'tax/weee/apply_vat' => '1', + ] + ] + ]; + } + + /** + * Test FPT disabled feature + * + * FPT enabled : FALSE + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceDisabledFPTSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + */ + public function testCatalogPriceDisableFPT(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10, 'delete' => '']] + ); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertEquals(100, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertCount( + 0, + $product['price_range']['minimum_price']['fixed_product_taxes'], + 'Fixed product tax count is incorrect' + ); + $this->assertResponseFields( + $product['price_range']['minimum_price']['fixed_product_taxes'], + [] + ); + } + + /** + * CatalogPriceDisableFPT settings data provider + * + * @return array + */ + public function catalogPriceDisabledFPTSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/weee/enable' => '0', + 'tax/weee/display' => '1', + ], + ], + ]; + } + + /** + * Get GraphQl query to fetch products by sku + * + * @param array $skus + * @return string + */ + private function getProductQuery(array $skus): string + { + $stringSkus = '"' . implode('","', $skus) . '"'; + return <<<QUERY +{ + products(filter: {sku: {in: [$stringSkus]}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + fixed_product_taxes{ + amount{value} + label + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + fixed_product_taxes + { + amount{value} + label + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php new file mode 100644 index 0000000000000..451ea78ee308d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Weee\Model\Config; + +/** + * Test for storeConfig FPT config values + */ +class StoreConfigFPTTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() :void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * FPT All Display settings + * + * @param array $weeTaxSettings + * @param string $displayValue + * @return void + * + * @dataProvider sameFPTDisplaySettingsProvider + */ + public function testSameFPTDisplaySettings(array $weeTaxSettings, $displayValue) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals($displayValue, $result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['sales_fixed_product_tax_display_setting']); + } + + /** + * SameFPTDisplaySettings settings data provider + * + * @return array + */ + public function sameFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplayIncludedOnly' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL, + ], + 'displayValue' => 'INCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + ], + 'displayValue' => 'INCLUDE_FPT_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndExcludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ], + 'displayValue' => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'EXCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '0', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'FPT_DISABLED', + ], + ]; + } + + /** + * FPT Display setting shuffled + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider differentFPTDisplaySettingsProvider + */ + public function testDifferentFPTDisplaySettings(array $weeTaxSettings) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals( + 'INCLUDE_FPT_WITHOUT_DETAILS', + $result['storeConfig']['product_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'INCLUDE_FPT_WITH_DETAILS', + $result['storeConfig']['category_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + $result['storeConfig']['sales_fixed_product_tax_display_setting'] + ); + } + + /** + * DifferentFPTDisplaySettings settings data provider + * + * @return array + */ + public function differentFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplay' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ] + ], + ]; + } + + /** + * Get GraphQl query to fetch storeConfig and FPT serttings + * + * @return string + */ + private function getStoreConfigQuery(): string + { + return <<<QUERY +{ + storeConfig { + product_fixed_product_tax_display_setting + category_fixed_product_tax_display_setting + sales_fixed_product_tax_display_setting + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php new file mode 100644 index 0000000000000..46844438fdd97 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Api; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Tests web-api for multishipping quote. + */ +class CartRepositoryTest extends WebapiAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + $this->sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); + $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $cart = $this->getCart('multishipping_quote_id'); + $quoteRepository->delete($cart); + } catch (\InvalidArgumentException $e) { + // Do nothing if cart fixture was not used + } + parent::tearDown(); + } + + /** + * Tests that multishipping quote contains all addresses in shipping assignments. + * + * @magentoApiDataFixture Magento/Multishipping/Fixtures/quote_with_split_items.php + */ + public function testGetMultishippingCart() + { + $cart = $this->getCart('multishipping_quote_id'); + $cartId = $cart->getId(); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/carts/' . $cartId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'quoteCartRepositoryV1', + 'serviceVersion' => 'V1', + 'operation' => 'quoteCartRepositoryV1Get', + ], + ]; + + $requestData = ['cartId' => $cartId]; + $cartData = $this->_webApiCall($serviceInfo, $requestData); + + $shippingAssignments = $cart->getExtensionAttributes()->getShippingAssignments(); + foreach ($shippingAssignments as $key => $shippingAssignment) { + $address = $shippingAssignment->getShipping()->getAddress(); + $cartItem = $shippingAssignment->getItems()[0]; + $this->assertEquals( + $address->getId(), + $cartData['extension_attributes']['shipping_assignments'][$key]['shipping']['address']['id'] + ); + $this->assertEquals( + $cartItem->getSku(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['sku'] + ); + $this->assertEquals( + $cartItem->getQty(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['qty'] + ); + } + } + + /** + * Retrieve quote by given reserved order ID + * + * @param string $reservedOrderId + * @return Quote + * @throws \InvalidArgumentException + */ + private function getCart(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { + throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); + } + + return array_pop($items); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php index 8cb82f5c8f206..5a894758dc9ed 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php @@ -42,6 +42,9 @@ class CartRepositoryTest extends WebapiAbstract */ private $filterBuilder; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -59,8 +62,10 @@ protected function setUp() protected function tearDown() { try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); $cart = $this->getCart('test01'); - $cart->delete(); + $quoteRepository->delete($cart); } catch (\InvalidArgumentException $e) { // Do nothing if cart fixture was not used } @@ -74,18 +79,27 @@ protected function tearDown() * @return \Magento\Quote\Model\Quote * @throws \InvalidArgumentException */ - protected function getCart($reservedOrderId) + private function getCart($reservedOrderId) { - /** @var $cart \Magento\Quote\Model\Quote */ - $cart = $this->objectManager->get(\Magento\Quote\Model\Quote::class); - $cart->load($reservedOrderId, 'reserved_order_id'); - if (!$cart->getId()) { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); } - return $cart; + + return array_pop($items); } /** + * Tests successfull get cart web-api call. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetCart() @@ -130,6 +144,8 @@ public function testGetCart() } /** + * Tests exception when cartId is not provided. + * * @expectedException \Exception * @expectedExceptionMessage No such entity with */ @@ -154,6 +170,8 @@ public function testGetCartThrowsExceptionIfThereIsNoCartWithProvidedId() } /** + * Tests carts search. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetList() @@ -184,6 +202,7 @@ public function testGetList() $this->searchCriteriaBuilder->addFilters([$grandTotalFilter, $subtotalFilter]); $this->searchCriteriaBuilder->addFilters([$minCreatedAtFilter]); $this->searchCriteriaBuilder->addFilters([$maxCreatedAtFilter]); + $this->searchCriteriaBuilder->addFilter('reserved_order_id', 'test01'); /** @var SortOrder $sortOrder */ $sortOrder = $this->sortOrderBuilder->setField('subtotal')->setDirection(SortOrder::SORT_ASC)->create(); $this->searchCriteriaBuilder->setSortOrders([$sortOrder]); diff --git a/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php new file mode 100644 index 0000000000000..5cbaa76631c23 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Swatches/Api/ProductAttributeOptionManagementInterfaceTest.php @@ -0,0 +1,225 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Swatches\Api; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Swatches\Model\ResourceModel\Swatch\Collection; +use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory; +use Magento\Swatches\Model\Swatch; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Test product attribute option management API for swatch attribute type + */ +class ProductAttributeOptionManagementInterfaceTest extends WebapiAbstract +{ + private const ATTRIBUTE_CODE = 'select_attribute'; + private const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products/attributes'; + + /** + * Test add option to swatch attribute + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + * @param array $data + * @param array $payload + * @param string $expectedSwatchType + * @param string $expectedLabel + * @param string $expectedValue + * + * @dataProvider addDataProvider + */ + public function testAdd( + array $data, + array $payload, + string $expectedSwatchType, + string $expectedLabel, + string $expectedValue + ) { + $objectManager = Bootstrap::getObjectManager(); + /** @var $attributeRepository AttributeRepository */ + $attributeRepository = $objectManager->get(AttributeRepository::class); + /** @var $attribute Attribute */ + $attribute = $attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, self::ATTRIBUTE_CODE); + $attribute->addData($data); + $attributeRepository->save($attribute); + $response = $this->_webApiCall( + [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . self::ATTRIBUTE_CODE . '/options', + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'add', + ], + ], + [ + 'attributeCode' => self::ATTRIBUTE_CODE, + 'option' => $payload, + ] + ); + + $this->assertNotNull($response); + $optionId = (int) ltrim($response, 'id_'); + $swatch = $this->getSwatch($optionId); + $this->assertEquals($expectedValue, $swatch->getValue()); + $this->assertEquals($expectedSwatchType, $swatch->getType()); + $options = $attribute->setStoreId(0)->getOptions(); + $this->assertCount(3, $options); + $this->assertEquals($expectedLabel, $options[2]->getLabel()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function addDataProvider() + { + return [ + 'visual swatch option with value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Black', + AttributeOptionInterface::VALUE => '#000000', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Noir', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_VISUAL_COLOR, + 'expectedLabel' => 'Black', + 'expectedValue' => '#000000', + ], + 'visual swatch option without value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Black', + AttributeOptionInterface::VALUE => '', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Noir', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_EMPTY, + 'expectedLabel' => 'Black', + 'expectedValue' => '', + ], + 'text swatch option with value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => 'S', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Petit', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Small', + 'expectedValue' => 'S', + ], + 'text swatch option without value' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => '', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Petit', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Small', + 'expectedValue' => '', + ], + 'text swatch option with value - redeclare store ID 0 in store_labels' => [ + 'data' => [ + Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_TEXT, + 'option' => [ + + ] + ], + 'payload' => [ + AttributeOptionInterface::LABEL => 'Small', + AttributeOptionInterface::VALUE => 'S', + AttributeOptionInterface::SORT_ORDER => 3, + AttributeOptionInterface::IS_DEFAULT => true, + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Slim', + AttributeOptionLabelInterface::STORE_ID => 0, + ], + ], + ], + 'expectedSwatchType' => Swatch::SWATCH_TYPE_TEXTUAL, + 'expectedLabel' => 'Slim', + 'expectedValue' => 'S', + ], + ]; + } + + /** + * Get swatch model + * + * @param int $optionId + * @return Swatch + */ + private function getSwatch(int $optionId) + { + /** @var Collection $collection */ + $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)->create(); + $collection->addFieldToFilter('option_id', $optionId); + $collection->setPageSize(1); + return $collection->getFirstItem(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php index b1f86739786c2..37cb2317b5b65 100644 --- a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php +++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncScheduleMultiStoreTest.php @@ -8,7 +8,7 @@ namespace Magento\WebapiAsync\Model; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\TestFramework\MessageQueue\PreconditionFailedException; use Magento\TestFramework\MessageQueue\PublisherConsumerController; use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException; @@ -19,7 +19,6 @@ use Magento\Framework\Registry; use Magento\Framework\Webapi\Exception; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\Store; use Magento\Framework\Webapi\Rest\Request; @@ -128,6 +127,13 @@ protected function setUp() */ public function testAsyncScheduleBulkMultistore($storeCode) { + if ($storeCode === self::STORE_CODE_FROM_FIXTURE) { + /** @var \Magento\Config\Model\Config $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config::class); + if (strpos($config->getConfigDataValue('catalog/search/engine'), 'elasticsearch') !== false) { + $this->markTestSkipped('MC-20452'); + } + } $product = $this->getProductData(); $this->_markTestAsRestOnly(); @@ -277,9 +283,9 @@ public function getProductData() 'product' => $productBuilder( [ - ProductInterface::TYPE_ID => 'simple', - ProductInterface::SKU => 'multistore-sku-test-1', - ProductInterface::NAME => 'Test Name ', + Product::TYPE_ID => 'simple', + Product::SKU => 'multistore-sku-test-1', + Product::NAME => 'Test Name ', ] ), ]; @@ -303,16 +309,16 @@ public function storeProvider() private function getSimpleProductData($productData = []) { return [ - ProductInterface::SKU => isset($productData[ProductInterface::SKU]) - ? $productData[ProductInterface::SKU] : uniqid('sku-', true), - ProductInterface::NAME => isset($productData[ProductInterface::NAME]) - ? $productData[ProductInterface::NAME] : uniqid('sku-', true), - ProductInterface::VISIBILITY => 4, - ProductInterface::TYPE_ID => 'simple', - ProductInterface::PRICE => 3.62, - ProductInterface::STATUS => 1, - ProductInterface::TYPE_ID => 'simple', - ProductInterface::ATTRIBUTE_SET_ID => 4, + Product::SKU => isset($productData[Product::SKU]) + ? $productData[Product::SKU] : uniqid('sku-', true), + Product::NAME => isset($productData[Product::NAME]) + ? $productData[Product::NAME] : uniqid('sku-', true), + Product::VISIBILITY => 4, + Product::TYPE_ID => 'simple', + Product::PRICE => 3.62, + Product::STATUS => 1, + Product::TYPE_ID => 'simple', + Product::ATTRIBUTE_SET_ID => 4, ]; } diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php index 49f2577b26211..bc3ae83643d3e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/LiselectstoreElement.php @@ -9,7 +9,6 @@ use Magento\Mtf\Client\Locator; /** - * Class LiselectstoreElement * Typified element class for lists selectors */ class LiselectstoreElement extends SimpleElement @@ -76,6 +75,7 @@ public function setValue($value) $option = $this->context->find($optionSelector, Locator::SELECTOR_XPATH); if (!$option->isVisible()) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('[' . implode('/', $value) . '] option is not visible in store switcher.'); } $option->click(); @@ -133,7 +133,7 @@ public function getValues() */ protected function isSubstring($haystack, $pattern) { - return preg_match("/$pattern/", $haystack) != 0 ? true : false; + return preg_match("/$pattern/", $haystack) != 0; } /** @@ -157,8 +157,8 @@ protected function findNearestElement($criteria, $key, array $elements) /** * Get selected store value * - * @throws \Exception * @return string + * @throws \Exception */ public function getValue() { diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml index 2d7e609c1c389..0694966c7eaa5 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/ConfigPageVisibilityTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\Backend\Test\TestCase\ConfigPageVisibilityTest" summary="Check Developer section and Locale field"> <variation name="VisibilityOfDeveloperSectionAndLocaleField" summary="Check Developer section and Locale field" ticketId="MAGETWO-63625, MAGETWO-63624"> <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Backend\Test\Constraint\AssertLocaleCodeVisibility" /> <constraint name="Magento\Backend\Test\Constraint\AssertDeveloperSectionVisibility" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php index f1ab255013280..a06ee2332704a 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Catalog/Product/View/Type/Bundle.php @@ -311,7 +311,7 @@ public function fillBundleOptions($bundleOptions) { foreach ($bundleOptions as $option) { $selector = sprintf($this->bundleOptionBlock, $option['title']); - $useDefault = isset($option['use_default']) && strtolower($option['use_default']) == 'true' ? true : false; + $useDefault = isset($option['use_default']) && strtolower($option['use_default']) == 'true'; if (!$useDefault) { /** @var Option $optionBlock */ $optionBlock = $this->blockFactory->create( diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml index 525e6b47374a0..028dfc6d109ea 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/ProductForm.xml @@ -31,7 +31,7 @@ </category_ids> <quantity_and_stock_status composite="1"> <qty> - <selector>fieldset[data-index="container_quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"]</selector> + <selector>fieldset[data-index="quantity_and_stock_status_qty"] [name="product[quantity_and_stock_status][qty]"]</selector> </qty> <is_in_stock> <selector>[data-index="quantity_and_stock_status"] [name="product[quantity_and_stock_status][is_in_stock]"]</selector> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php index 23e0236fe7baa..70d2730868dbf 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/TaxClass.php @@ -52,7 +52,7 @@ class TaxClass extends DataSource public function __construct(FixtureFactory $fixtureFactory, array $params, $data = []) { $this->params = $params; - if ((!isset($data['dataset']) && !isset($data['tax_product_class']))) { + if (!isset($data['dataset']) && !isset($data['tax_product_class'])) { $this->data = $data; return; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml index 5fa1cfe5e5911..732dac98e0779 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml @@ -11,6 +11,7 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">configurableProduct::default</data> <data name="actionName" xsi:type="string">-</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -34,6 +35,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> <data name="actionName" xsi:type="string">deleteVariations</data> @@ -48,6 +50,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">-</data> @@ -60,6 +63,7 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> @@ -71,6 +75,7 @@ <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableLinksData" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> @@ -78,6 +83,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation9"> + <data name="tag" xsi:type="string">test_type:acceptance_test</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">clearDownloadableData</data> @@ -97,6 +103,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml index 0ff402daca07d..4f74b7ff554db 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Block/Adminhtml/Promo/Catalog/Edit/PromoForm.xml @@ -12,7 +12,7 @@ <strategy>css selector</strategy> <fields> <is_active> - <input>select</input> + <input>switcher</input> </is_active> <website_ids> <selector>[name='website_ids']</selector> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php index 8ac13d407e433..695323990063a 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleInGrid.php @@ -19,7 +19,7 @@ class AssertCatalogPriceRuleInGrid extends AbstractConstraint * Fields used to filter rows in the grid. * @var array */ - protected $fieldsToFilter = ['name', 'is_active']; + protected $fieldsToFilter = ['name']; /** * Assert that data in grid on Catalog Price Rules page according to fixture diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml index 49bf36b0325ba..1f16e28c067d8 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/CreateCatalogPriceRuleEntityTest.xml @@ -10,7 +10,7 @@ <variation name="CatalogRule_Create_Active_AdminOnly"> <data name="catalogPriceRule/data/name" xsi:type="string">CatalogPriceRule %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">Catalog Price Rule Description</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">Wholesale</data> <data name="catalogPriceRule/data/simple_action" xsi:type="string">Apply as percentage of original</data> @@ -24,7 +24,7 @@ <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogPriceRule/data/name" xsi:type="string">CatalogPriceRule %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">Catalog Price Rule Description</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Inactive</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">No</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">General</data> <data name="catalogPriceRule/data/condition" xsi:type="string">-</data> @@ -39,7 +39,7 @@ <variation name="CatalogRule_Create_ForGuestUsers_AdjustPriceToPercentage"> <data name="product" xsi:type="string">MAGETWO-23036</data> <data name="catalogPriceRule/data/name" xsi:type="string">rule_name%isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">NOT LOGGED IN</data> <data name="conditionEntity" xsi:type="string">category</data> @@ -54,7 +54,7 @@ <data name="customer/dataset" xsi:type="string">customer_with_new_customer_group</data> <data name="product" xsi:type="string">simple_10_dollar</data> <data name="catalogPriceRule/data/name" xsi:type="string">rule_name%isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="conditionEntity" xsi:type="string">category</data> <data name="catalogPriceRule/data/conditions" xsi:type="string">[Category|is|%category_id%]</data> @@ -68,7 +68,7 @@ <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> <data name="product" xsi:type="string">product_with_custom_color_attribute</data> <data name="catalogPriceRule/data/name" xsi:type="string">Catalog Price Rule %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/website_ids/option_0" xsi:type="string">Main Website</data> <data name="catalogPriceRule/data/customer_group_ids/option_0" xsi:type="string">NOT LOGGED IN</data> <data name="conditionEntity" xsi:type="string">attribute</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml index 6c8e86b24ae60..e2916432c8eb7 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/UpdateCatalogPriceRuleEntityTest.xml @@ -10,7 +10,7 @@ <variation name="CatalogRule_Update_Name_Status"> <data name="catalogPriceRuleOriginal/dataset" xsi:type="string">active_catalog_price_rule_with_conditions</data> <data name="catalogPriceRule/data/name" xsi:type="string">New Catalog Price Rule Name %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Inactive</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">No</data> <data name="saveAction" xsi:type="string">save</data> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleSuccessSaveMessage" /> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleNoticeMessage" /> @@ -24,7 +24,7 @@ <data name="catalogPriceRuleOriginal/dataset" xsi:type="string">active_catalog_price_rule_with_conditions</data> <data name="catalogPriceRule/data/name" xsi:type="string">New Catalog Price Rule Name %isolation%</data> <data name="catalogPriceRule/data/description" xsi:type="string">New Catalog Price Rule Description %isolation%</data> - <data name="catalogPriceRule/data/is_active" xsi:type="string">Active</data> + <data name="catalogPriceRule/data/is_active" xsi:type="string">Yes</data> <data name="catalogPriceRule/data/conditions" xsi:type="string">[Category|is|%category_1%]</data> <data name="catalogPriceRule/data/simple_action" xsi:type="string">Apply as fixed amount</data> <data name="catalogPriceRule/data/discount_amount" xsi:type="string">35</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml index 4744fa7756c4e..9a26386c82cb8 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/AdvancedSearchEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\AdvancedSearchEntityTest" summary="Use Advanced Search" ticketId="MAGETWO-24729"> <variation name="AdvancedSearchEntityTestVariation1" summary="Use Advanced Search to Find the Product" ticketId="MAGETWO-12421"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> <data name="productSearch/data/name" xsi:type="string">abc_dfj</data> @@ -16,6 +16,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -23,6 +24,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial name</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -30,6 +32,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by sku</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -37,6 +40,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial sku</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -44,6 +48,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial sku and description</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -52,6 +57,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by description</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -59,6 +65,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by short description</data> <data name="products/simple_1" xsi:type="string">-</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -66,6 +73,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by partial short description</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -73,6 +81,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">Yes</data> @@ -80,6 +89,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -88,6 +98,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation12"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name, sku, description, short description, price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -100,6 +111,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation13"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Search product in advanced search by name, sku, description, short description, price from and price to</data> <data name="products/simple_1" xsi:type="string">Yes</data> <data name="products/simple_2" xsi:type="string">-</data> @@ -112,11 +124,13 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductsResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Negative product search</data> <data name="productSearch/data/name" xsi:type="string">Negative_product_search</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchNoResult" /> </variation> <variation name="AdvancedSearchEntityTestVariation15" summary="Do Advanced Search without entering data" ticketId="MAGETWO-14859"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="issue" xsi:type="string">MAGETWO-18537: "Enter a search term and try again." error message is missed in Advanced Search</data> <data name="productSearch/data/name" xsi:type="string" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchEmptyTerm" /> diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml index 95d99f9fa76cd..f98f9ca7cfe24 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Block/Adminhtml/Block/Agreement/Edit/AgreementsForm.xml @@ -18,7 +18,7 @@ <input>select</input> </mode> <stores> - <selector>[name="stores[0]"]</selector> + <selector>[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <checkbox_text /> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php index 61166339475b7..dc1e901a3feae 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Form/CustomerForm.php @@ -29,7 +29,7 @@ class CustomerForm extends Form * * @var string */ - protected $customerAttribute = "[orig-name='%s[]']"; + protected $customerAttribute = "[name='%s[]']"; /** * Validation text message for a field. diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml index 9c9c917f8a66d..48129ef287498 100644 --- a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/FilterProductListTest.xml @@ -94,7 +94,9 @@ <constraint name="Magento\LayeredNavigation\Test\Constraint\AssertFilterProductList" /> </variation> <variation name="FilterProductListTestVariation4" summary="Use sorting category filter when layered navigation is applied" ticketId="MAGETWO-42701"> + <data name="tag" xsi:type="string">test_type:mysql_search</data> <data name="configData" xsi:type="string">layered_navigation_manual_range_10</data> + <data name="runReindex" xsi:type="boolean">true</data> <data name="category/dataset" xsi:type="string">default_anchor_subcategory</data> <data name="category/data/category_products/dataset" xsi:type="string">catalogProductSimple::product_10_dollar, catalogProductSimple::product_20_dollar, configurableProduct::filterable_two_options_with_zero_price</data> <data name="layeredNavigation" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml b/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml index 4d2acc76c8703..c1970955013e8 100644 --- a/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml +++ b/dev/tests/functional/tests/app/Magento/Newsletter/Test/Block/Adminhtml/Queue/Edit/QueueForm.xml @@ -11,7 +11,7 @@ <selector>input[name='start_at']</selector> </queue_start_at> <stores> - <selector>select[name="stores[0]"]</selector> + <selector>select[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <newsletter_subject> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml index 51809448e4edb..d66c3b702f076 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/Coupons/Filter.xml @@ -29,7 +29,7 @@ <input>select</input> </price_rule_type> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <rules_list> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml index 5820de6772e1c..08e783e1329a4 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Block/Adminhtml/Sales/TaxRule/Filter.xml @@ -23,7 +23,7 @@ <input>select</input> </show_order_statuses> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <show_empty_rows> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml index 504ce64bf2a73..3e1a1c727c668 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/Block/Adminhtml/Rating/Edit/RatingForm.xml @@ -12,7 +12,7 @@ <strategy>css selector</strategy> <fields> <stores> - <selector>[name="stores[0]"]</selector> + <selector>[name="stores[]"]</selector> <input>multiselectgrouplist</input> </stores> <is_active> diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php index e7dd72d1d426c..da5e7101e4b33 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/MassActionsProductReviewEntityTest.php @@ -102,7 +102,7 @@ public function test($gridActions, $gridStatus) $this->reviewIndex->getReviewGrid()->massaction( [['title' => $this->review->getTitle()]], [$gridActions => $gridStatus], - ($gridActions == 'Delete' ? true : false) + ($gridActions == 'Delete') ); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml index 294f64966bde9..d868798eba79d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Report/Filter/Form.xml @@ -26,7 +26,7 @@ <input>select</input> </show_order_statuses> <order_statuses> - <selector>[name="order_statuses[0]"]</selector> + <selector>[name="order_statuses[]"]</selector> <input>multiselect</input> </order_statuses> <show_actual_columns> diff --git a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/elastic_search.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/elastic_search.xml new file mode 100644 index 0000000000000..6141151518332 --- /dev/null +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/elastic_search.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../vendor/magento/mtf/Magento/Mtf/TestRunner/etc/testRunner.xsd"> + <rule scope="testcase"> + <deny> + <tag group="stable" value="no" /> + <tag group="to_maintain" value="yes" /> + <tag group="mftf_migrated" value="yes" /> + </deny> + </rule> + <rule scope="testsuite"> + <deny> + <module value="Magento_Setup" strict="1" /> + <module value="Magento_SampleData" strict="1" /> + </deny> + </rule> + <rule scope="variation"> + <deny> + <tag group="test_type" value="3rd_party_test, 3rd_party_test_single_flow, mysql_search" /> + <tag group="stable" value="no" /> + <tag group="mftf_migrated" value="yes" /> + <tag group="to_maintain" value="yes" /> + </deny> + </rule> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/etc/module.xml new file mode 100644 index 0000000000000..bae0739d237e1 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleCatalogSearch"> + <sequence> + <module name="Magento_CatalogSearch"/> + </sequence> + </module> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php new file mode 100644 index 0000000000000..78fb97a9e1134 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogSearch/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogSearch', __DIR__); +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php index 9f697a1be6339..cd9512c227893 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php @@ -6,6 +6,9 @@ namespace Magento\TestFramework\Mail\Template; +/** + * Class TransportBuilderMock + */ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBuilder { /** @@ -38,11 +41,12 @@ public function getSentMessage() * Return transport mock. * * @return \Magento\TestFramework\Mail\TransportInterfaceMock + * @throws \Magento\Framework\Exception\LocalizedException */ public function getTransport() { $this->prepareMessage(); $this->reset(); - return new \Magento\TestFramework\Mail\TransportInterfaceMock(); + return new \Magento\TestFramework\Mail\TransportInterfaceMock($this->message); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php index 8f967b501a59f..5bf98b76e7d59 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php @@ -6,8 +6,28 @@ namespace Magento\TestFramework\Mail; +use Magento\Framework\Mail\EmailMessageInterface; + +/** + * Class TransportInterfaceMock + */ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterface { + /** + * @var null|EmailMessageInterface + */ + private $message; + + /** + * TransportInterfaceMock constructor. + * + * @param null|EmailMessageInterface $message + */ + public function __construct($message = null) + { + $this->message = $message; + } + /** * Mock of send a mail using transport * @@ -15,16 +35,17 @@ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterfa */ public function sendMessage() { + //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired return; } /** * Get message * - * @return string + * @return null|EmailMessageInterface */ public function getMessage() { - return ''; + return $this->message; } } diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php index 794e589002e73..fa3869d49bd2a 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Customer/PlaceOrderWithAuthorizeNetTest.php @@ -108,7 +108,7 @@ public function testDispatchToPlaceOrderWithRegisteredCustomer(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -142,12 +142,12 @@ public function testDispatchToPlaceOrderWithRegisteredCustomer(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php index 070543a0880e8..4946448f91ccc 100644 --- a/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetGraphQl/Model/Resolver/Guest/PlaceOrderWithAuthorizeNetTest.php @@ -108,7 +108,7 @@ public function testDispatchToPlaceAnOrderWithAuthorizenet(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -137,12 +137,12 @@ public function testDispatchToPlaceAnOrderWithAuthorizenet(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php index 5ca2bf1f73175..5ef518fd2152f 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Auth; +use Magento\TestFramework\Bootstrap as TestHelper; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoAppArea adminhtml * @magentoAppIsolation enabled @@ -18,10 +22,15 @@ class SessionTest extends \PHPUnit\Framework\TestCase private $auth; /** - * @var \Magento\Backend\Model\Auth\Session + * @var Session */ private $authSession; + /** + * @var SessionFactory + */ + private $authSessionFactory; + /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -30,11 +39,12 @@ class SessionTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); - $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->authSession = $this->objectManager->create(Session::class); + $this->authSessionFactory = $this->objectManager->get(SessionFactory::class); $this->auth->setAuthStorage($this->authSession); $this->auth->logout(); } @@ -52,8 +62,8 @@ public function testIsLoggedIn($loggedIn) { if ($loggedIn) { $this->auth->login( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestHelper::ADMIN_NAME, + TestHelper::ADMIN_PASSWORD ); } $this->assertEquals($loggedIn, $this->authSession->isLoggedIn()); @@ -63,4 +73,55 @@ public function loginDataProvider() { return [[false], [true]]; } + + /** + * Check that persisting user data is working. + */ + public function testStorage() + { + $this->auth->login(TestHelper::ADMIN_NAME, TestHelper::ADMIN_PASSWORD); + $user = $this->authSession->getUser(); + $acl = $this->authSession->getAcl(); + /** @var Session $session */ + $session = $this->authSessionFactory->create(); + $persistedUser = $session->getUser(); + $persistedAcl = $session->getAcl(); + + $this->assertEquals($user->getData(), $persistedUser->getData()); + $this->assertEquals($user->getAclRole(), $persistedUser->getAclRole()); + $this->assertEquals($acl->getRoles(), $persistedAcl->getRoles()); + $this->assertEquals($acl->getResources(), $persistedAcl->getResources()); + } + + /** + * Check that session manager can work with user storage in the old way. + */ + public function testInnerStorage(): void + { + /** @var \Magento\Framework\Session\StorageInterface $innerStorage */ + $innerStorage = Bootstrap::getObjectManager()->get(\Magento\Framework\Session\StorageInterface::class); + $this->authSession = $this->authSessionFactory->create(['storage' => $innerStorage]); + $this->auth->login(TestHelper::ADMIN_NAME, TestHelper::ADMIN_PASSWORD); + $user = $this->auth->getAuthStorage()->getUser(); + $acl = $this->auth->getAuthStorage()->getAcl(); + $this->assertNotEmpty($user); + $this->assertNotEmpty($acl); + $this->auth->logout(); + $this->assertEmpty($this->auth->getAuthStorage()->getUser()); + $this->assertEmpty($this->auth->getAuthStorage()->getAcl()); + $this->authSession->setUser($user); + $this->authSession->setAcl($acl); + $this->assertTrue($user === $this->authSession->getUser()); + $this->assertTrue($acl === $this->authSession->getAcl()); + $this->authSession->destroy(); + $innerStorage->setUser($user); + $innerStorage->setAcl($acl); + $this->assertTrue($user === $this->authSession->getUser()); + $this->assertTrue($acl === $this->authSession->getAcl()); + /** @var Session $newSession */ + $newSession = $this->authSessionFactory->create(['storage' => $innerStorage]); + $this->assertTrue($newSession->hasUser()); + $this->assertTrue($newSession->hasAcl()); + $this->assertEquals($user->getId(), $newSession->getUser()->getId()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php index d1252be2c4b53..a930244238efa 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Locale; use Magento\Framework\Locale\Resolver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; /** * @magentoAppArea adminhtml @@ -17,16 +20,19 @@ class ResolverTest extends \PHPUnit\Framework\TestCase */ protected $_model; + /** + * {@inheritDoc} + */ protected function setUp() { parent::setUp(); - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->_model = Bootstrap::getObjectManager()->create( \Magento\Backend\Model\Locale\Resolver::class ); } /** - * @covers \Magento\Backend\Model\Locale\Resolver::setLocale + * Tests setLocale() with default locale */ public function testSetLocaleWithDefaultLocale() { @@ -34,16 +40,16 @@ public function testSetLocaleWithDefaultLocale() } /** - * @covers \Magento\Backend\Model\Locale\Resolver::setLocale + * Tests setLocale() with interface locale */ public function testSetLocaleWithBaseInterfaceLocale() { - $user = new \Magento\Framework\DataObject(); - $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $user = Bootstrap::getObjectManager()->create(User::class); + $session = Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class ); $session->setUser($user); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class )->getUser()->setInterfaceLocale( 'fr_FR' @@ -52,11 +58,11 @@ public function testSetLocaleWithBaseInterfaceLocale() } /** - * @covers \Magento\Backend\Model\Locale\Resolver::setLocale + * Tests setLocale() with session locale */ public function testSetLocaleWithSessionLocale() { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Session::class )->setSessionLocale( 'es_ES' @@ -65,23 +71,55 @@ public function testSetLocaleWithSessionLocale() } /** - * @covers \Magento\Backend\Model\Locale\Resolver::setLocale + * Tests setLocale() with post parameter */ public function testSetLocaleWithRequestLocale() { - $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $request = Bootstrap::getObjectManager() ->get(\Magento\Framework\App\RequestInterface::class); $request->setPostValue(['locale' => 'de_DE']); $this->_checkSetLocale('de_DE'); } + /** + * Tests setLocale() with parameter + * + * @param string|null $localeParam + * @param string|null $localeRequestParam + * @param string $localeExpected + * @dataProvider setLocaleWithParameterDataProvider + */ + public function testSetLocaleWithParameter( + ?string $localeParam, + ?string $localeRequestParam, + string $localeExpected + ) { + $request = Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\RequestInterface::class); + $request->setPostValue(['locale' => $localeRequestParam]); + $this->_model->setLocale($localeParam); + $this->assertEquals($localeExpected, $this->_model->getLocale()); + } + + /** + * @return array + */ + public function setLocaleWithParameterDataProvider(): array + { + return [ + ['ko_KR', 'ja_JP', 'ja_JP'], + ['ko_KR', null, 'ko_KR'], + [null, 'ja_JP', 'ja_JP'], + ]; + } + /** * Check set locale * * @param string $localeCodeToCheck * @return void */ - protected function _checkSetLocale($localeCodeToCheck) + private function _checkSetLocale($localeCodeToCheck) { $this->_model->setLocale(); $localeCode = $this->_model->getLocale(); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php index d6ea08a2f7ca3..55d8c6a6a2170 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Invoice/CreateTest.php @@ -62,7 +62,7 @@ protected function tearDown() * during creation second partial invoice. * * @return void - * @magentoConfigFixture default_store payment/braintree/merchant_account_id Magneto + * @magentoConfigFixture default_store payment/braintree/merchant_account_id Magento * @magentoConfigFixture current_store payment/braintree/merchant_account_id USA_Merchant * @magentoDataFixture Magento/Braintree/Fixtures/partial_invoice.php */ @@ -71,11 +71,14 @@ public function testCreatePartialInvoiceWithNonDefaultMerchantAccount(): void $order = $this->getOrder('100000002'); $this->adapter->method('sale') - ->with(self::callback(function ($request) { - self::assertEquals('USA_Merchant', $request['merchantAccountId']); - return true; - })) - ->willReturn($this->getTransactionStub()); + ->with( + self::callback( + function ($request) { + self::assertEquals('USA_Merchant', $request['merchantAccountId']); + return true; + } + ) + )->willReturn($this->getTransactionStub()); $uri = 'backend/sales/order_invoice/save/order_id/' . $order->getEntityId(); $this->prepareRequest($uri); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php new file mode 100644 index 0000000000000..4c598c16c3c47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml/ProductTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Controller\Adminhtml; + +use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for product admin controllers. + * @magentoAppArea adminhtml + */ +class ProductTest extends AbstractBackendController +{ + /** + * Test bundle product duplicate won't remove bundle options from original product. + * + * @magentoDataFixture Magento/Catalog/_files/products_new.php + * @return void + */ + public function testDuplicateProduct() + { + $params = $this->getRequestParamsForDuplicate(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams(['type' => Type::TYPE_BUNDLE]); + $this->getRequest()->setPostValue($params); + $this->dispatch('backend/catalog/product/save'); + $this->assertSessionMessages( + $this->equalTo( + [ + 'You saved the product.', + 'You duplicated the product.', + ] + ), + MessageInterface::TYPE_SUCCESS + ); + $this->assertOptions(); + } + + /** + * Get necessary request post params for creating and duplicating bundle product. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getRequestParamsForDuplicate() + { + $product = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class)->get('simple'); + return [ + 'product' => + [ + 'attribute_set_id' => '4', + 'gift_message_available' => '0', + 'use_config_gift_message_available' => '1', + 'stock_data' => + [ + 'min_qty_allowed_in_shopping_cart' => + [ + [ + 'record_id' => '0', + 'customer_group_id' => '32000', + 'min_sale_qty' => '', + ], + ], + 'min_qty' => '0', + 'max_sale_qty' => '10000', + 'notify_stock_qty' => '1', + 'min_sale_qty' => '1', + 'qty_increments' => '1', + 'use_config_manage_stock' => '1', + 'manage_stock' => '1', + 'use_config_min_qty' => '1', + 'use_config_max_sale_qty' => '1', + 'use_config_backorders' => '1', + 'backorders' => '0', + 'use_config_notify_stock_qty' => '1', + 'use_config_enable_qty_inc' => '1', + 'enable_qty_increments' => '0', + 'use_config_qty_increments' => '1', + 'use_config_min_sale_qty' => '1', + 'is_qty_decimal' => '0', + 'is_decimal_divided' => '0', + ], + 'status' => '1', + 'affect_product_custom_options' => '1', + 'name' => 'b1', + 'price' => '', + 'weight' => '', + 'url_key' => '', + 'special_price' => '', + 'quantity_and_stock_status' => + [ + 'qty' => '', + 'is_in_stock' => '1', + ], + 'sku_type' => '0', + 'price_type' => '0', + 'weight_type' => '0', + 'website_ids' => + [ + 1 => '1', + ], + 'sku' => 'b1', + 'meta_title' => 'b1', + 'meta_keyword' => 'b1', + 'meta_description' => 'b1 ', + 'tax_class_id' => '2', + 'product_has_weight' => '1', + 'visibility' => '4', + 'country_of_manufacture' => '', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_layout' => '', + 'price_view' => '0', + 'shipment_type' => '0', + 'news_from_date' => '', + 'news_to_date' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'special_from_date' => '', + 'special_to_date' => '', + 'description' => '', + 'short_description' => '', + 'custom_layout_update' => '', + 'image' => '', + 'small_image' => '', + 'thumbnail' => '', + ], + 'bundle_options' => + [ + 'bundle_options' => + [ + [ + 'record_id' => '0', + 'type' => 'select', + 'required' => '1', + 'title' => 'test option title', + 'position' => '1', + 'option_id' => '', + 'delete' => '', + 'bundle_selections' => + [ + [ + 'product_id' => $product->getId(), + 'name' => $product->getName(), + 'sku' => $product->getSku(), + 'price' => $product->getPrice(), + 'delete' => '', + 'selection_can_change_qty' => '', + 'selection_id' => '', + 'selection_price_type' => '0', + 'selection_price_value' => '', + 'selection_qty' => '1', + 'position' => '1', + 'option_id' => '', + 'record_id' => '1', + 'is_default' => '0', + ], + ], + 'bundle_button_proxy' => + [ + [ + 'entity_id' => '1', + ], + ], + ], + ], + ], + 'affect_bundle_product_selections' => '1', + 'back' => 'duplicate', + 'form_key' => Bootstrap::getObjectManager()->get(FormKey::class)->getFormKey(), + ]; + } + + /** + * Check options in created and duplicated products. + * + * @return void + */ + private function assertOptions() + { + $createdOptions = $this->getProductOptions('b1'); + $createdOption = array_shift($createdOptions); + $duplicatedOptions = $this->getProductOptions('b1-1'); + $duplicatedOption = array_shift($duplicatedOptions); + $this->assertNotEmpty($createdOption); + $this->assertNotEmpty($duplicatedOption); + $optionFields = ['type', 'title', 'position', 'required', 'default_title']; + foreach ($optionFields as $field) { + $this->assertSame($createdOption->getData($field), $duplicatedOption->getData($field)); + } + $createdLinks = $createdOption->getProductLinks(); + $createdLink = array_shift($createdLinks); + $duplicatedLinks = $duplicatedOption->getProductLinks(); + $duplicatedLink = array_shift($duplicatedLinks); + $this->assertNotEmpty($createdLink); + $this->assertNotEmpty($duplicatedLink); + $linkFields = [ + 'entity_id', + 'sku', + 'position', + 'is_default', + 'price', + 'qty', + 'selection_can_change_quantity', + 'price_type', + ]; + foreach ($linkFields as $field) { + $this->assertSame($createdLink->getData($field), $duplicatedLink->getData($field)); + } + } + + /** + * Get options for given product. + * + * @param string $sku + * @return OptionInterface[] + */ + private function getProductOptions(string $sku) + { + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = $product->getResource()->getIdBySku($sku); + $product->load($productId); + + return $product->getExtensionAttributes()->getBundleProductOptions(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php index 51250580eb6ae..77c1ade0fae3f 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/TypeTest.php @@ -49,19 +49,20 @@ protected function setUp() /** * @magentoDataFixture Magento/Bundle/_files/product.php - * @covers \Magento\Indexer\Model\Indexer::reindexAll * @covers \Magento\Bundle\Model\Product\Type::getSearchableData * @magentoDbIsolation disabled */ - public function testPrepareProductIndexForBundleProduct() + public function testGetSearchableData() { - $this->indexer->reindexAll(); - - $select = $this->connectionMock->select()->from($this->resource->getTableName('catalogsearch_fulltext_scope1')) - ->where('`data_index` LIKE ?', '%' . 'Bundle Product Items' . '%'); + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Model\Product $bundleProduct */ + $bundleProduct = $productRepository->get('bundle-product'); + $bundleType = $bundleProduct->getTypeInstance(); + /** @var \Magento\Bundle\Model\Product\Type $bundleType */ + $searchableData = $bundleType->getSearchableData($bundleProduct); - $result = $this->connectionMock->fetchAll($select); - $this->assertCount(1, $result); + $this->assertCount(1, $searchableData); + $this->assertEquals('Bundle Product Items', $searchableData[0]); } /** diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php new file mode 100644 index 0000000000000..c00fd2435a9f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../../Magento/Catalog/_files/multiple_products.php'; + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = [10, 11, 12]; +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(0) + ->setSkuType(1) + ->setWeightType(1) + ->setPriceType(0) + ->setPrice(10.0) + ->setSpecialPrice(10) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'delete' => '', + ], + + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_price_value' => 2.75, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 6.75, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $link->setPrice($linkData['selection_price_value']); + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +/** @var $tierPriceExtensionAttributesFactory */ +$tierPriceExtensionAttributesFactory = $objectManager->create(ProductTierPriceExtensionFactory::class); +$tierPriceExtensionAttribute = $tierPriceExtensionAttributesFactory->create()->setPercentageValue(10); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttribute); +$product->setTierPrices($tierPrices); +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php new file mode 100644 index 0000000000000..a17e2604d9d01 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/dynamic_bundle_product_with_multiple_options_rollback.php @@ -0,0 +1,7 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../../Magento/Bundle/_files/product_with_multiple_options_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php new file mode 100644 index 0000000000000..90f354d90f17a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UrlRewriteTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Class defines url rewrite creation for category save controller + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UrlRewriteTest extends AbstractBackendController +{ + /** @var $urlRewriteCollectionFactory */ + private $urlRewriteCollectionFactory; + + /** @var Json */ + private $jsonSerializer; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->urlRewriteCollectionFactory = $this->_objectManager->get(UrlRewriteCollectionFactory::class); + $this->jsonSerializer = $this->_objectManager->get(Json::class); + } + + /** + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @dataProvider categoryDataProvider + * @param array $data + * @return void + */ + public function testUrlRewrite(array $data): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($data); + $this->dispatch('backend/catalog/category/save'); + $categoryId = $this->jsonSerializer->unserialize($this->getResponse()->getBody())['category']['entity_id']; + $this->assertNotNull($categoryId, 'The category was not created'); + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $urlRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, ['eq' => $categoryId]) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE]); + $this->assertCount( + 1, + $urlRewriteCollection->getItems(), + 'Wrong count of url rewrites was created' + ); + } + + /** + * @return array + */ + public function categoryDataProvider(): array + { + return [ + 'url_rewrite_is_created_during_category_save' => [ + [ + 'path' => '1/2', + 'name' => 'Custom Name', + 'parent' => 2, + 'is_active' => '0', + 'include_in_menu' => '1', + 'display_mode' => 'PRODUCTS', + 'is_anchor' => true, + 'return_session_messages_only' => true, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'filter_price_range' => 1, + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 1001d58ee8a67..c0eeb75592a5d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -3,39 +3,58 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category as Category; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Store\Model\Store; -use Magento\Catalog\Model\ResourceModel\Product; /** + * Test for category backend actions + * * @magentoAppArea adminhtml */ -class CategoryTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class CategoryTest extends AbstractBackendController { /** - * @var \Magento\Catalog\Model\ResourceModel\Product + * @var ProductResource */ protected $productResource; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreRepositoryInterface */ + private $storeRepository; + + /** @var Json */ + private $json; + /** - * @inheritDoc - * - * @throws \Magento\Framework\Exception\AuthenticationException + * @inheritdoc */ protected function setUp() { parent::setUp(); - - /** @var Product $productResource */ - $this->productResource = Bootstrap::getObjectManager()->get( - Product::class - ); + /** @var ProductResource $productResource */ + $this->productResource = $this->_objectManager->get(ProductResource::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeRepository = $this->_objectManager->get(StoreRepositoryInterface::class); + $this->json = $this->_objectManager->get(Json::class); } /** + * Test save action. + * * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDbIsolation enabled * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 @@ -43,35 +62,23 @@ protected function setUp() * @param array $inputData * @param array $defaultAttributes * @param array $attributesSaved - * @param bool $isSuccess + * @return void */ - public function testSaveAction($inputData, $defaultAttributes, $attributesSaved = [], $isSuccess = true) + public function testSaveAction(array $inputData, array $defaultAttributes, array $attributesSaved = []): void { - /** @var $store \Magento\Store\Model\Store */ - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); - $store->load('fixturestore', 'code'); + $store = $this->storeRepository->get('fixturestore'); $storeId = $store->getId(); - $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->getRequest()->setParam('store', $storeId); $this->getRequest()->setParam('id', 2); $this->dispatch('backend/catalog/category/save'); - - if ($isSuccess) { - $this->assertSessionMessages( - $this->equalTo(['You saved the category.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class + $this->assertSessionMessages( + $this->equalTo(['You saved the category.']), + MessageInterface::TYPE_SUCCESS ); - $category->setStoreId($storeId); - $category->load(2); - + /** @var $category Category */ + $category = $this->categoryRepository->get(2, $storeId); $errors = []; foreach ($attributesSaved as $attribute => $value) { $actualValue = $category->getData($attribute); @@ -95,11 +102,53 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved } /** + * Check default value for category url path + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories.php + * @return void + */ + public function testDefaultValueForCategoryUrlPath(): void + { + $categoryId = 3; + $category = $this->categoryRepository->get($categoryId); + $newUrlPath = 'test_url_path'; + $defaultUrlPath = $category->getData('url_path'); + + // update url_path and check it + $category->setStoreId(1); + $category->setUrlKey($newUrlPath); + $category->setUrlPath($newUrlPath); + $this->categoryRepository->save($category); + $this->assertEquals($newUrlPath, $category->getUrlPath()); + + // set default url_path and check it + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $postData = $category->getData(); + $postData['use_default'] = [ + 'available_sort_by' => 1, + 'default_sort_by' => 1, + 'url_key' => 1, + ]; + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the category.')]), + MessageInterface::TYPE_SUCCESS + ); + $category = $this->categoryRepository->get($categoryId); + $this->assertEquals($defaultUrlPath, $category->getData('url_path')); + } + + /** + * Test save action from product form page + * * @param array $postData * @dataProvider categoryCreatedFromProductCreationPageDataProvider * @magentoDbIsolation enabled + * @return void */ - public function testSaveActionFromProductCreationPage($postData) + public function testSaveActionFromProductCreationPage(array $postData): void { $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); @@ -112,11 +161,7 @@ public function testSaveActionFromProductCreationPage($postData) $this->stringContains('http://localhost/index.php/backend/catalog/category/edit/') ); } else { - $result = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Json\Helper\Data::class - )->jsonDecode( - $body - ); + $result = $this->json->unserialize($body); $this->assertArrayHasKey('messages', $result); $this->assertFalse($result['error']); $category = $result['category']; @@ -130,10 +175,12 @@ public function testSaveActionFromProductCreationPage($postData) } /** + * Get category post data + * * @static * @return array */ - public static function categoryCreatedFromProductCreationPageDataProvider() + public static function categoryCreatedFromProductCreationPageDataProvider(): array { /* Keep in sync with new-category-dialog.js */ $postData = [ @@ -152,8 +199,10 @@ public static function categoryCreatedFromProductCreationPageDataProvider() /** * Test SuggestCategories finds any categories. + * + * @return void */ - public function testSuggestCategoriesActionDefaultCategoryFound() + public function testSuggestCategoriesActionDefaultCategoryFound(): void { $this->getRequest()->setParam('label_part', 'Default'); $this->dispatch('backend/catalog/category/suggestCategories'); @@ -165,8 +214,10 @@ public function testSuggestCategoriesActionDefaultCategoryFound() /** * Test SuggestCategories properly processes search by label. + * + * @return void */ - public function testSuggestCategoriesActionNoSuggestions() + public function testSuggestCategoriesActionNoSuggestions(): void { $this->getRequest()->setParam('label_part', strrev('Default')); $this->dispatch('backend/catalog/category/suggestCategories'); @@ -174,10 +225,12 @@ public function testSuggestCategoriesActionNoSuggestions() } /** + * Save action data provider + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @return array */ - public function saveActionDataProvider() + public function saveActionDataProvider(): array { return [ 'default values' => [ @@ -301,64 +354,39 @@ public function saveActionDataProvider() 'filter_price_range' => null ], ], - 'incorrect datefrom' => [ - [ - 'id' => '2', - 'entity_id' => '2', - 'path' => '1/2', - 'name' => 'Custom Name', - 'is_active' => '0', - 'description' => 'Custom Description', - 'meta_title' => 'Custom Title', - 'meta_keywords' => 'Custom keywords', - 'meta_description' => 'Custom meta description', - 'include_in_menu' => '0', - 'url_key' => 'default-category', - 'display_mode' => 'PRODUCTS', - 'landing_page' => '1', - 'is_anchor' => true, - 'custom_apply_to_products' => '0', - 'custom_design' => 'Magento/blank', - 'custom_design_from' => '5/29/2015', - 'custom_design_to' => '5/21/2015', - 'page_layout' => '', - 'custom_layout_update' => '', - 'use_config' => [ - 'available_sort_by' => 1, - 'default_sort_by' => 1, - 'filter_price_range' => 1, - ], - ], - [ - 'name' => false, - 'default_sort_by' => false, - 'display_mode' => false, - 'meta_title' => false, - 'custom_design' => false, - 'page_layout' => false, - 'is_active' => false, - 'include_in_menu' => false, - 'landing_page' => false, - 'custom_apply_to_products' => false, - 'available_sort_by' => false, - 'description' => false, - 'meta_keywords' => false, - 'meta_description' => false, - 'custom_layout_update' => false, - 'custom_design_from' => false, - 'custom_design_to' => false, - 'filter_price_range' => false - ], - [], - false - ] ]; } + /** + * @magentoDbIsolation enabled + * @return void + */ + public function testIncorrectDateFrom(): void + { + $data = [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + 'custom_design_from' => '5/29/2015', + 'custom_design_to' => '5/21/2015', + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($data); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('Make sure the To Date is later than or the same as the From Date.')]), + MessageInterface::TYPE_ERROR + ); + } + /** * Test validation. + * + * @return void */ - public function testSaveActionCategoryWithDangerRequest() + public function testSaveActionCategoryWithDangerRequest(): void { $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( @@ -377,11 +405,13 @@ public function testSaveActionCategoryWithDangerRequest() $this->dispatch('backend/catalog/category/save'); $this->assertSessionMessages( $this->equalTo(['The "Name" attribute value is empty. Set the attribute and try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + MessageInterface::TYPE_ERROR ); } /** + * Test move action. + * * @magentoDataFixture Magento/Catalog/_files/category_tree.php * @dataProvider moveActionDataProvider * @@ -391,18 +421,23 @@ public function testSaveActionCategoryWithDangerRequest() * @param int $grandChildId * @param string $grandChildUrlKey * @param boolean $error + * @return void */ - public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, $grandChildUrlKey, $error) - { + public function testMoveAction( + int $parentId, + int $childId, + string $childUrlKey, + int $grandChildId, + string $grandChildUrlKey, + bool $error + ): void { $urlKeys = [ $childId => $childUrlKey, $grandChildId => $grandChildUrlKey, ]; foreach ($urlKeys as $categoryId => $urlKey) { - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + /** @var $category Category */ + $category = $this->_objectManager->create(Category::class); if ($categoryId > 0) { $category->load($categoryId) ->setUrlKey($urlKey) @@ -414,15 +449,17 @@ public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, ->setPostValue('pid', $parentId) ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/category/move'); - $jsonResponse = json_decode($this->getResponse()->getBody()); + $jsonResponse = $this->json->unserialize($this->getResponse()->getBody()); $this->assertNotNull($jsonResponse); - $this->assertEquals($error, $jsonResponse->error); + $this->assertEquals($error, $jsonResponse['error']); } /** + * Move action data provider + * * @return array */ - public function moveActionDataProvider() + public function moveActionDataProvider(): array { return [ [400, 401, 'first_url_key', 402, 'second_url_key', false], @@ -433,17 +470,17 @@ public function moveActionDataProvider() } /** + * Test save category with product position. + * * @magentoDataFixture Magento/Catalog/_files/products_in_different_stores.php * @magentoDbIsolation disabled * @dataProvider saveActionWithDifferentWebsitesDataProvider * * @param array $postData */ - public function testSaveCategoryWithProductPosition(array $postData) + public function testSaveCategoryWithProductPosition(array $postData): void { - /** @var $store \Magento\Store\Model\Store */ - $store = Bootstrap::getObjectManager()->create(Store::class); - $store->load('fixturestore', 'code'); + $store = $this->storeRepository->get('fixturestore'); $storeId = $store->getId(); $oldCategoryProductsCount = $this->getCategoryProductsCount(); $this->getRequest()->setParam('store', $storeId); @@ -451,6 +488,10 @@ public function testSaveCategoryWithProductPosition(array $postData) $this->getRequest()->setParam('id', 96377); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the category.')]), + MessageInterface::TYPE_SUCCESS + ); $newCategoryProductsCount = $this->getCategoryProductsCount(); $this->assertEquals( $oldCategoryProductsCount, @@ -460,10 +501,12 @@ public function testSaveCategoryWithProductPosition(array $postData) } /** + * Save action data provider + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @return array */ - public function saveActionWithDifferentWebsitesDataProvider() + public function saveActionWithDifferentWebsitesDataProvider(): array { return [ 'default_values' => [ @@ -477,7 +520,6 @@ public function saveActionWithDifferentWebsitesDataProvider() 'path' => '1/2/96377', 'level' => '2', 'children_count' => '0', - 'row_id' => '96377', 'name' => 'Category 1', 'display_mode' => 'PRODUCTS', 'url_key' => 'category-1', @@ -541,7 +583,7 @@ public function saveActionWithDifferentWebsitesDataProvider() } /** - * Get items count from catalog_category_product + * Get items count from catalog_category_product. * * @return int */ @@ -555,4 +597,36 @@ private function getCategoryProductsCount(): int $this->productResource->getConnection()->fetchAll($oldCategoryProducts) ); } + + /** + * Verify that the category cannot be saved if the category url matches the admin url. + * + * @magentoConfigFixture admin/url/use_custom_path 1 + * @magentoConfigFixture admin/url/custom_path backend + */ + public function testSaveWithCustomBackendNameAction() + { + $frontNameResolver = Bootstrap::getObjectManager()->create(FrontNameResolver::class); + $urlKey = $frontNameResolver->getFrontName(); + $inputData = [ + 'id' => '2', + 'url_key' => $urlKey, + 'use_config' => [ + 'available_sort_by' => 1, + 'default_sort_by' => 1 + ] + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($inputData); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + $this->equalTo( + [ + 'URL key "backend" matches a reserved endpoint name ' + . '(admin, soap, rest, graphql, standard, backend). Use another URL key.' + ] + ), + MessageInterface::TYPE_ERROR + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php new file mode 100644 index 0000000000000..6fb74d54d827f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php @@ -0,0 +1,278 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Gallery; + +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\Filesystem\DirectoryList as AppDirectoryList; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for admin product upload image action. + * + * @magentoAppArea adminhtml + */ +class UploadTest extends AbstractBackendController +{ + /** + * @inheritdoc + */ + protected $resource = 'Magento_Catalog::products'; + + /** + * @inheritdoc + */ + protected $uri = 'backend/catalog/product_gallery/upload'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Json + */ + private $serializer; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Config + */ + private $config; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->httpMethod = HttpRequest::METHOD_POST; + $this->filesystem = $this->_objectManager->get(Filesystem::class); + $this->serializer = $this->_objectManager->get(Json::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(AppDirectoryList::MEDIA); + $this->config = $this->_objectManager->get(Config::class); + } + + /** + * Test upload image on admin product page. + * + * @dataProvider uploadActionDataProvider + * @magentoDbIsolation enabled + * @param array $file + * @param array $expectation + * @return void + */ + public function testUploadAction(array $file, array $expectation): void + { + $this->copyFileToSysTmpDir($file); + $this->getRequest()->setMethod($this->httpMethod); + $this->dispatch($this->uri); + $jsonBody = $this->serializer->unserialize($this->getResponse()->getBody()); + $this->assertEquals($jsonBody['name'], $expectation['name']); + $this->assertEquals($jsonBody['type'], $expectation['type']); + $this->assertEquals($jsonBody['file'], $expectation['file']); + $this->assertEquals($jsonBody['url'], $expectation['url']); + $this->assertArrayNotHasKey('error', $jsonBody); + $this->assertArrayNotHasKey('errorcode', $jsonBody); + $this->assertFileExists( + $this->getFileAbsolutePath($expectation['tmp_media_path']) + ); + } + + /** + * @return array + */ + public function uploadActionDataProvider(): array + { + return [ + 'upload_image_with_type_jpg' => [ + 'file' => [ + 'name' => 'magento_image.jpg', + 'type' => 'image/jpeg', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'name' => 'magento_image.jpg', + 'type' => 'image/jpeg', + 'file' => '/m/a/magento_image.jpg.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.jpg', + 'tmp_media_path' => '/m/a/magento_image.jpg', + ], + ], + 'upload_image_with_type_png' => [ + 'file' => [ + 'name' => 'product_image.png', + 'type' => 'image/png', + 'current_path' => '/../../../../controllers/_files', + ], + 'expectation' => [ + 'name' => 'product_image.png', + 'type' => 'image/png', + 'file' => '/p/r/product_image.png.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/p/r/product_image.png', + 'tmp_media_path' => '/p/r/product_image.png', + ], + ], + 'upload_image_with_type_gif' => [ + 'file' => [ + 'name' => 'magento_image.gif', + 'type' => 'image/gif', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'name' => 'magento_image.gif', + 'type' => 'image/gif', + 'file' => '/m/a/magento_image.gif.tmp', + 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.gif', + 'tmp_media_path' => '/m/a/magento_image.gif', + ], + ], + ]; + } + + /** + * Test upload image on admin product page. + * + * @dataProvider uploadActionWithErrorsDataProvider + * @magentoDbIsolation enabled + * @param array $file + * @param array $expectation + * @return void + */ + public function testUploadActionWithErrors(array $file, array $expectation): void + { + if (!empty($file['create_file'])) { + $this->createFileInSysTmpDir($file['name']); + } elseif (!empty($file['copy_file'])) { + $this->copyFileToSysTmpDir($file); + } + + $this->getRequest()->setMethod($this->httpMethod); + $this->dispatch($this->uri); + $jsonBody = $this->serializer->unserialize($this->getResponse()->getBody()); + $this->assertEquals($jsonBody['error'], $expectation['message']); + $this->assertEquals($jsonBody['errorcode'], $expectation['errorcode']); + + if (!empty($expectation['tmp_media_path'])) { + $this->assertFileNotExists( + $this->getFileAbsolutePath($expectation['tmp_media_path']) + ); + } + } + + /** + * @return array + */ + public function uploadActionWithErrorsDataProvider(): array + { + return [ + 'upload_image_with_invalid_type' => [ + 'file' => [ + 'create_file' => true, + 'name' => 'invalid_file.txt', + ], + 'expectation' => [ + 'message' => 'Disallowed file type.', + 'errorcode' => 0, + 'tmp_media_path' => '/i/n/invalid_file.txt', + ], + ], + 'upload_empty_image' => [ + 'file' => [ + 'copy_file' => true, + 'name' => 'magento_empty.jpg', + 'type' => 'image/jpeg', + 'current_path' => '/../../../../_files', + ], + 'expectation' => [ + 'message' => 'Wrong file size.', + 'errorcode' => 0, + 'tmp_media_path' => '/m/a/magento_empty.jpg', + ], + ], + 'upload_without_image' => [ + 'file' => [], + 'expectation' => [ + 'message' => '$_FILES array is empty', + 'errorcode' => 0, + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $_FILES = []; + $this->mediaDirectory->delete('tmp'); + parent::tearDown(); + } + + /** + * Copies file to tmp dir. + * + * @param array $file + * @return void + */ + private function copyFileToSysTmpDir(array $file): void + { + if (!empty($file)) { + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $fixtureDir = realpath(__DIR__ . $file['current_path']); + $filePath = $tmpDirectory->getAbsolutePath($file['name']); + copy($fixtureDir . DIRECTORY_SEPARATOR . $file['name'], $filePath); + + $_FILES['image'] = [ + 'name' => $file['name'], + 'type' => $file['type'], + 'tmp_name' => $filePath, + ]; + } + } + + /** + * Creates txt file with given name and copies to tmp dir. + * + * @param string $name + * @return void + */ + private function createFileInSysTmpDir(string $name): void + { + $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $filePath = $tmpDirectory->getAbsolutePath($name); + $file = fopen($filePath, "wb"); + fwrite($file, 'some text'); + + $_FILES['image'] = [ + 'name' => $name, + 'type' => 'text/plain', + 'tmp_name' => $filePath, + ]; + } + + /** + * Returns absolute path to file in media tmp dir. + * + * @param string $tmpPath + * @return string + */ + private function getFileAbsolutePath(string $tmpPath): string + { + return $this->mediaDirectory->getAbsolutePath($this->config->getBaseTmpMediaPath() . $tmpPath); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php new file mode 100644 index 0000000000000..80f15da647b25 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/CreateCustomOptionsTest.php @@ -0,0 +1,271 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Base test cases for product custom options with type "field". + * Option add via dispatch product controller action save with options data in POST data. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class CreateCustomOptionsTest extends AbstractBackendController +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $optionRepository; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + $this->optionRepository = $this->_objectManager->create(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test add to product custom option with type "field". + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productWithNewOptionsDataProvider + * + * @param array $productPostData + */ + public function testSaveCustomOptionWithTypeField(array $productPostData): void + { + $this->getRequest()->setPostValue($productPostData); + $product = $this->productRepository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + $productOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(2, $productOptions); + foreach ($productOptions as $customOption) { + $postOptionData = $productPostData['product']['options'][$customOption->getTitle()] ?? null; + $this->assertNotNull($postOptionData); + $this->assertEquals($postOptionData['title'], $customOption->getTitle()); + $this->assertEquals($postOptionData['type'], $customOption->getType()); + $this->assertEquals($postOptionData['is_require'], $customOption->getIsRequire()); + $this->assertEquals($postOptionData['sku'], $customOption->getSku()); + $this->assertEquals($postOptionData['price'], $customOption->getPrice()); + $this->assertEquals($postOptionData['price_type'], $customOption->getPriceType()); + $maxCharacters = $postOptionData['max_characters'] ?? 0; + $this->assertEquals($maxCharacters, $customOption->getMaxCharacters()); + } + } + + /** + * Return all data for add option to product for all cases. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productWithNewOptionsDataProvider(): array + { + return [ + 'required_options' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'not_required_options' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 0, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'options_with_fixed_price' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + ], + ], + ], + 'options_with_percent_price' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 20, + 'price_type' => 'percent', + ], + ], + ], + ], + ], + 'options_with_max_charters_configuration' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'max_characters' => 50, + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + 'options_without_max_charters_configuration' => [ + [ + 'product' => [ + 'options' => [ + 'Test option title 1' => [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + 'Test option title 2' => [ + 'record_id' => 1, + 'sort_order' => 2, + 'is_require' => 1, + 'sku' => 'test-option-title-2', + 'title' => 'Test option title 2', + 'type' => 'field', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php new file mode 100644 index 0000000000000..697980d75a715 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Save/ImagesTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Save; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Provide tests for admin product save action with images. + * + * @magentoAppArea adminhtml + */ +class ImagesTest extends AbstractBackendController +{ + /** + * @var Config + */ + private $config; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->config = $this->_objectManager->get(Config::class); + $this->mediaDirectory = $this->_objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + } + + /** + * Test save product with default image. + * + * @dataProvider simpleProductImagesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @magentoDbIsolation enabled + * @param array $postData + * @param array $expectation + * @return void + */ + public function testSaveSimpleProductDefaultImage(array $postData, array $expectation): void + { + $product = $this->productRepository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->equalTo(['You saved the product.']), + MessageInterface::TYPE_SUCCESS + ); + $this->assertSuccessfulImageSave($expectation); + } + + /** + * @return array + */ + public function simpleProductImagesDataProvider(): array + { + return [ + 'simple_product_with_jpg_image' => [ + 'post_data' => [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + 'lrwuv5ukisn' => [ + 'position' => '1', + 'media_type' => 'image', + 'video_provider' => '', + 'file' => '/m/a//magento_image.jpg.tmp', + 'value_id' => '', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'role' => '', + ], + ], + ], + 'image' => '/m/a//magento_image.jpg.tmp', + 'small_image' => '/m/a//magento_image.jpg.tmp', + 'thumbnail' => '/m/a//magento_image.jpg.tmp', + 'swatch_image' => '/m/a//magento_image.jpg.tmp', + ], + ], + 'expectation' => [ + 'media_gallery_image' => [ + 'position' => '1', + 'media_type' => 'image', + 'file' => '/m/a/magento_image.jpg', + 'label' => '', + 'disabled' => '0', + ], + 'image' => '/m/a/magento_image.jpg', + 'small_image' => '/m/a/magento_image.jpg', + 'thumbnail' => '/m/a/magento_image.jpg', + 'swatch_image' => '/m/a/magento_image.jpg', + ] + ] + ]; + } + + /** + * @param array $expectation + * @return void + */ + private function assertSuccessfulImageSave(array $expectation): void + { + $product = $this->productRepository->get('simple', false, null, true); + $galleryImage = reset($product->getData('media_gallery')['images']); + $expectedGalleryImage = $expectation['media_gallery_image']; + $this->assertEquals($expectedGalleryImage['position'], $galleryImage['position']); + $this->assertEquals($expectedGalleryImage['media_type'], $galleryImage['media_type']); + $this->assertEquals($expectedGalleryImage['label'], $galleryImage['label']); + $this->assertEquals($expectedGalleryImage['disabled'], $galleryImage['disabled']); + $this->assertEquals($expectedGalleryImage['file'], $galleryImage['file']); + $this->assertEquals($expectation['image'], $product->getData('image')); + $this->assertEquals($expectation['small_image'], $product->getData('small_image')); + $this->assertEquals($expectation['thumbnail'], $product->getData('thumbnail')); + $this->assertEquals($expectation['swatch_image'], $product->getData('swatch_image')); + $this->assertFileExists( + $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $expectation['image']) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php index 8ccd426424a29..187fddae1ce4f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product\Set; use Magento\Eav\Api\AttributeSetRepositoryInterface; @@ -10,9 +12,86 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Eav\Api\AttributeManagementInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Developer\Model\Logger\Handler\Syslog; +use Magento\Framework\Logger\Monolog; +use Magento\Catalog\Model\Product\Attribute\Repository; +/** + * Test save attribute set + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var string + */ + private $systemLogPath = ''; + + /** + * @var Monolog + */ + private $logger; + + /** + * @var Syslog + */ + private $syslogHandler; + + /** + * @var AttributeManagementInterface + */ + private $attributeManagement; + + /** + * @var DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Repository + */ + private $attributeRepository; + + /** + * @inheritDoc + */ + public function setUp() + { + parent::setUp(); + $this->logger = $this->_objectManager->get(Monolog::class); + $this->syslogHandler = $this->_objectManager->create( + Syslog::class, + [ + 'filePath' => Bootstrap::getInstance()->getAppTempDir(), + ] + ); + $this->attributeManagement = $this->_objectManager->get(AttributeManagementInterface::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->attributeRepository = $this->_objectManager->get(Repository::class); + $this->dataObjectHelper = $this->_objectManager->get(DataObjectHelper::class); + } + + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function tearDown() + { + $this->attributeRepository->get('country_of_manufacture')->setIsUserDefined(false); + parent::tearDown(); + } + /** * @magentoDataFixture Magento/Catalog/_files/attribute_set_with_renamed_group.php */ @@ -22,17 +101,22 @@ public function testAlreadyExistsExceptionProcessingWhenGroupCodeIsDuplicated() $this->assertNotEmpty($attributeSet, 'Attribute set with name "attribute_set_test" is missed'); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->getRequest()->setPostValue('data', json_encode([ - 'attribute_set_name' => 'attribute_set_test', - 'groups' => [ - ['ynode-418', 'attribute-group-name', 1], - ], - 'attributes' => [ - ['9999', 'ynode-418', 1, null] - ], - 'not_attributes' => [], - 'removeGroups' => [], - ])); + $this->getRequest()->setPostValue( + 'data', + json_encode( + [ + 'attribute_set_name' => 'attribute_set_test', + 'groups' => [ + ['ynode-418', 'attribute-group-name', 1], + ], + 'attributes' => [ + ['9999', 'ynode-418', 1, null] + ], + 'not_attributes' => [], + 'removeGroups' => [], + ] + ) + ); $this->dispatch('backend/catalog/product_set/save/id/' . $attributeSet->getAttributeSetId()); $jsonResponse = json_decode($this->getResponse()->getBody()); @@ -63,4 +147,64 @@ protected function getAttributeSetByName($attributeSetName) $items = $result->getItems(); return $result->getTotalCount() ? array_pop($items) : null; } + + /** + * Test behavior when attribute set was changed to a new set + * with deleted attribute from the previous set + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Catalog/_files/attribute_set_based_on_default.php + * @magentoDbIsolation disabled + */ + public function testRemoveAttributeFromAttributeSet() + { + $message = 'Attempt to load value of nonexistent EAV attribute'; + $this->removeSyslog(); + $attributeSet = $this->getAttributeSetByName('new_attribute_set'); + $product = $this->productRepository->get('simple'); + $this->attributeRepository->get('country_of_manufacture')->setIsUserDefined(true); + $this->attributeManagement->unassign($attributeSet->getId(), 'country_of_manufacture'); + $productData = [ + 'country_of_manufacture' => 'Angola' + ]; + $this->dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); + $this->productRepository->save($product); + $product->setAttributeSetId($attributeSet->getId()); + $product = $this->productRepository->save($product); + $this->dispatch('backend/catalog/product/edit/id/' . $product->getEntityId()); + $syslogPath = $this->getSyslogPath(); + $syslogContent = file_exists($syslogPath) ? file_get_contents($syslogPath) : ''; + $this->assertNotContains($message, $syslogContent); + } + + /** + * Retrieve system.log file path + * + * @return string + */ + private function getSyslogPath(): string + { + if (!$this->systemLogPath) { + foreach ($this->logger->getHandlers() as $handler) { + if ($handler instanceof \Magento\Framework\Logger\Handler\System) { + $this->systemLogPath = $handler->getUrl(); + } + } + } + + return $this->systemLogPath; + } + + /** + * Remove system.log file + * + * @return void + */ + private function removeSyslog() + { + $this->syslogHandler->close(); + if (file_exists($this->getSyslogPath())) { + unlink($this->getSyslogPath()); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index acec996d0c406..3a28801b1ace1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml; use Magento\Framework\App\Request\DataPersistorInterface; @@ -10,8 +12,12 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Message\MessageInterface; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\CacheCleaner; /** + * Test class for Product adminhtml actions + * * @magentoAppArea adminhtml */ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendController @@ -58,11 +64,10 @@ public function testSaveActionAndNew() */ public function testSaveActionAndDuplicate() { - $this->getRequest()->setPostValue(['back' => 'duplicate']); $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var Product $product */ $product = $repository->get('simple'); - $this->getRequest()->setMethod(HttpRequest::METHOD_POST); - $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSaveAndDuplicateAction($product); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/edit/')); $this->assertRedirect( $this->logicalNot( @@ -71,14 +76,30 @@ public function testSaveActionAndDuplicate() ) ) ); - $this->assertSessionMessages( - $this->contains('You saved the product.'), - MessageInterface::TYPE_SUCCESS - ); - $this->assertSessionMessages( - $this->contains('You duplicated the product.'), - MessageInterface::TYPE_SUCCESS - ); + } + + /** + * Tests of saving and duplicating existing product after the script execution. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSaveActionAndDuplicateWithUrlPathAttribute() + { + $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + /** @var Product $product */ + $product = $repository->get('simple'); + + // set url_path attribute and check it + $product->setData('url_path', $product->getSku()); + $repository->save($product); + $urlPathAttribute = $product->getCustomAttribute('url_path'); + $this->assertEquals($urlPathAttribute->getValue(), $product->getSku()); + + // clean cache + CacheCleaner::cleanAll(); + + // dispatch Save&Duplicate action and check it + $this->assertSaveAndDuplicateAction($product); } /** @@ -355,4 +376,24 @@ private function getProductData(array $tierPrice) unset($product['entity_id']); return $product; } + + /** + * Dispatch Save&Duplicate action and check it + * + * @param Product $product + */ + private function assertSaveAndDuplicateAction(Product $product) + { + $this->getRequest()->setPostValue(['back' => 'duplicate']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + $this->assertSessionMessages( + $this->contains('You duplicated the product.'), + MessageInterface::TYPE_SUCCESS + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 1d7936d740b8d..5ec0427093997 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -3,26 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category as Category; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\Tree; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; +use Magento\Framework\Url; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * Test class for \Magento\Catalog\Model\Category. * - general behaviour is tested * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @see \Magento\Catalog\Model\CategoryTreeTest * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ -class CategoryTest extends \PHPUnit\Framework\TestCase +class CategoryTest extends TestCase { /** - * @var \Magento\Store\Model\Store + * @var Store */ protected $_store; /** - * @var \Magento\Catalog\Model\Category + * @var Category */ protected $_model; @@ -31,50 +49,61 @@ class CategoryTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** @var CategoryRepository */ + private $categoryResource; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** + * @inheritdoc + */ protected function setUp() { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var $storeManager \Magento\Store\Model\StoreManagerInterface */ - $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + /** @var $storeManager StoreManagerInterface */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->_store = $storeManager->getStore(); - $this->_model = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + $this->_model = $this->objectManager->create(Category::class); + $this->categoryResource = $this->objectManager->get(CategoryResource::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); } - public function testGetUrlInstance() + public function testGetUrlInstance(): void { $instance = $this->_model->getUrlInstance(); - $this->assertInstanceOf(\Magento\Framework\Url::class, $instance); + $this->assertInstanceOf(Url::class, $instance); $this->assertSame($instance, $this->_model->getUrlInstance()); } - public function testGetTreeModel() + public function testGetTreeModel(): void { $model = $this->_model->getTreeModel(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Category\Tree::class, $model); + $this->assertInstanceOf(Tree::class, $model); $this->assertNotSame($model, $this->_model->getTreeModel()); } - public function testGetTreeModelInstance() + public function testGetTreeModelInstance(): void { $model = $this->_model->getTreeModelInstance(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Category\Tree::class, $model); + $this->assertInstanceOf(Tree::class, $model); $this->assertSame($model, $this->_model->getTreeModelInstance()); } - public function testGetDefaultAttributeSetId() + public function testGetDefaultAttributeSetId(): void { /* based on value installed in DB */ $this->assertEquals(3, $this->_model->getDefaultAttributeSetId()); } - public function testGetProductCollection() + public function testGetProductCollection(): void { $collection = $this->_model->getProductCollection(); - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Product\Collection::class, $collection); + $this->assertInstanceOf(ProductCollection::class, $collection); $this->assertEquals($this->_model->getStoreId(), $collection->getStoreId()); } - public function testGetAttributes() + public function testGetAttributes(): void { $attributes = $this->_model->getAttributes(); $this->assertArrayHasKey('name', $attributes); @@ -85,7 +114,7 @@ public function testGetAttributes() $this->assertArrayNotHasKey('custom_design', $attributes); } - public function testGetProductsPosition() + public function testGetProductsPosition(): void { $this->assertEquals([], $this->_model->getProductsPosition()); $this->_model->unsetData(); @@ -97,23 +126,21 @@ public function testGetProductsPosition() $this->assertNotEmpty($this->_model->getProductsPosition()); } - public function testGetStoreIds() + public function testGetStoreIds(): void { $this->_model = $this->getCategoryByName('Category 1.1'); /* id from fixture */ $this->assertContains( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore()->getId(), + Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId(), $this->_model->getStoreIds() ); } - public function testSetGetStoreId() + public function testSetGetStoreId(): void { $this->assertEquals( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class + Bootstrap::getObjectManager()->get( + StoreManagerInterface::class )->getStore()->getId(), $this->_model->getStoreId() ); @@ -126,10 +153,10 @@ public function testSetGetStoreId() * @magentoAppIsolation enabled * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 */ - public function testSetStoreIdWithNonNumericValue() + public function testSetStoreIdWithNonNumericValue(): void { - /** @var $store \Magento\Store\Model\Store */ - $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + /** @var $store Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); $store->load('fixturestore'); $this->assertNotEquals($this->_model->getStoreId(), $store->getId()); @@ -139,7 +166,7 @@ public function testSetStoreIdWithNonNumericValue() $this->assertEquals($this->_model->getStoreId(), $store->getId()); } - public function testGetUrl() + public function testGetUrl(): void { $this->assertStringEndsWith('catalog/category/view/', $this->_model->getUrl()); @@ -156,42 +183,42 @@ public function testGetUrl() $this->assertStringEndsWith('catalog/category/view/id/1000/', $this->_model->getUrl()); } - public function testGetCategoryIdUrl() + public function testGetCategoryIdUrl(): void { $this->assertStringEndsWith('catalog/category/view/', $this->_model->getCategoryIdUrl()); $this->_model->setUrlKey('test_key'); $this->assertStringEndsWith('catalog/category/view/s/test_key/', $this->_model->getCategoryIdUrl()); } - public function testFormatUrlKey() + public function testFormatUrlKey(): void { $this->assertEquals('test', $this->_model->formatUrlKey('test')); $this->assertEquals('test-some-chars-5', $this->_model->formatUrlKey('test-some#-chars^5')); $this->assertEquals('test', $this->_model->formatUrlKey('test-????????')); } - public function testGetImageUrl() + public function testGetImageUrl(): void { $this->assertFalse($this->_model->getImageUrl()); $this->_model->setImage('test.gif'); $this->assertStringEndsWith('media/catalog/category/test.gif', $this->_model->getImageUrl()); } - public function testGetCustomDesignDate() + public function testGetCustomDesignDate(): void { $dates = $this->_model->getCustomDesignDate(); $this->assertArrayHasKey('from', $dates); $this->assertArrayHasKey('to', $dates); } - public function testGetDesignAttributes() + public function testGetDesignAttributes(): void { $attributes = $this->_model->getDesignAttributes(); $this->assertContains('custom_design_from', array_keys($attributes)); $this->assertContains('custom_design_to', array_keys($attributes)); } - public function testCheckId() + public function testCheckId(): void { $this->_model = $this->getCategoryByName('Category 1.1.1'); $categoryId = $this->_model->getId(); @@ -199,13 +226,13 @@ public function testCheckId() $this->assertFalse($this->_model->checkId(111)); } - public function testVerifyIds() + public function testVerifyIds(): void { $ids = $this->_model->verifyIds($this->_model->getParentIds()); $this->assertNotContains(100, $ids); } - public function testHasChildren() + public function testHasChildren(): void { $this->_model->load(3); $this->assertTrue($this->_model->hasChildren()); @@ -213,21 +240,21 @@ public function testHasChildren() $this->assertFalse($this->_model->hasChildren()); } - public function testGetRequestPath() + public function testGetRequestPath(): void { $this->assertNull($this->_model->getRequestPath()); $this->_model->setData('request_path', 'test'); $this->assertEquals('test', $this->_model->getRequestPath()); } - public function testGetName() + public function testGetName(): void { $this->assertNull($this->_model->getName()); $this->_model->setData('name', 'test'); $this->assertEquals('test', $this->_model->getName()); } - public function testGetProductCount() + public function testGetProductCount(): void { $this->_model->load(6); $this->assertEquals(0, $this->_model->getProductCount()); @@ -236,14 +263,14 @@ public function testGetProductCount() $this->assertEquals(1, $this->_model->getProductCount()); } - public function testGetAvailableSortBy() + public function testGetAvailableSortBy(): void { $this->assertEquals([], $this->_model->getAvailableSortBy()); $this->_model->setData('available_sort_by', 'test,and,test'); $this->assertEquals(['test', 'and', 'test'], $this->_model->getAvailableSortBy()); } - public function testGetAvailableSortByOptions() + public function testGetAvailableSortByOptions(): void { $options = $this->_model->getAvailableSortByOptions(); $this->assertContains('price', array_keys($options)); @@ -251,25 +278,27 @@ public function testGetAvailableSortByOptions() $this->assertContains('name', array_keys($options)); } - public function testGetDefaultSortBy() + public function testGetDefaultSortBy(): void { $this->assertEquals('position', $this->_model->getDefaultSortBy()); } - public function testValidate() + public function testValidate(): void { - $this->_model->addData([ - "include_in_menu" => false, - "is_active" => false, - 'name' => 'test', - ]); + $this->_model->addData( + [ + "include_in_menu" => false, + "is_active" => false, + 'name' => 'test', + ] + ); $this->assertNotEmpty($this->_model->validate()); } /** * @magentoDataFixture Magento/Catalog/_files/category_with_position.php */ - public function testSaveCategoryWithPosition() + public function testSaveCategoryWithPosition(): void { $category = $this->_model->load('444'); $this->assertEquals('5', $category->getPosition()); @@ -278,10 +307,10 @@ public function testSaveCategoryWithPosition() /** * @magentoDbIsolation enabled */ - public function testSaveCategoryWithoutImage() + public function testSaveCategoryWithoutImage(): void { - $model = $this->objectManager->create(\Magento\Catalog\Model\Category::class); - $repository = $this->objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + $model = $this->objectManager->create(Category::class); + $repository = $this->objectManager->get(CategoryRepositoryInterface::class); $model->setName('Test Category 100') ->setParentId(2) @@ -299,7 +328,7 @@ public function testSaveCategoryWithoutImage() /** * @magentoAppArea adminhtml */ - public function testDeleteChildren() + public function testDeleteChildren(): void { $this->_model->unsetData(); $this->_model->load(4); @@ -320,29 +349,101 @@ public function testDeleteChildren() $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDataFixture Magento/Catalog/_files/category.php + */ + public function testAddChildCategory(): void + { + $data = [ + 'name' => 'Child Category', + 'path' => '1/2/333', + 'is_active' => '1', + 'include_in_menu' => '1', + ]; + $this->_model->setData($data); + $this->categoryResource->save($this->_model); + $parentCategory = $this->categoryRepository->get(333); + $this->assertContains($this->_model->getId(), $parentCategory->getChildren()); + } + + /** + * @return void + */ + public function testMissingRequiredAttribute(): void + { + $data = [ + 'path' => '1/2', + 'is_active' => '1', + 'include_in_menu' => '1', + ]; + $this->expectException(AttributeException::class); + $this->expectExceptionMessage( + (string)__('The "Name" attribute value is empty. Set the attribute and try again.') + ); + $this->_model->setData($data); + $this->_model->validate(); + } + + /** + * @dataProvider categoryFieldsProvider + * @param array $data + */ + public function testCategoryCreateWithDifferentFields(array $data): void + { + $requiredData = [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + ]; + $this->_model->setData(array_merge($requiredData, $data)); + $this->categoryResource->save($this->_model); + $category = $this->categoryRepository->get($this->_model->getId()); + $categoryData = $category->toArray(array_keys($data)); + $this->assertSame($data, $categoryData); + } + + /** + * @return array + */ + public function categoryFieldsProvider(): array + { + return [ + [ + 'enable_fields' => [ + 'is_active' => '1', + 'include_in_menu' => '1', + ], + 'disable_fields' => [ + 'is_active' => '0', + 'include_in_menu' => '0', + ], + ], + ]; + } + /** * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation disabled * @return void */ - public function testCreateSubcategoryWithMultipleStores() + public function testCreateSubcategoryWithMultipleStores(): void { $parentCategoryId = 3; - $storeManager = $this->objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); - $storeManager->setCurrentStore(\Magento\Store\Model\Store::ADMIN_CODE); - /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ - $storeRepository = $this->objectManager->get(\Magento\Store\Api\StoreRepositoryInterface::class); + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(Store::ADMIN_CODE); + /** @var StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); $storeId = $storeRepository->get('fixture_second_store')->getId(); - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface $repository */ - $repository = $this->objectManager->get(\Magento\Catalog\Api\CategoryRepositoryInterface::class); + /** @var CategoryRepositoryInterface $repository */ + $repository = $this->objectManager->get(CategoryRepositoryInterface::class); $parentCategory = $repository->get($parentCategoryId, $storeId); $parentAllStoresPath = $parentCategory->getUrlPath(); $parentSecondStoreKey = 'parent-category-url-key-second-store'; $parentCategory->setUrlKey($parentSecondStoreKey); $repository->save($parentCategory); - /** @var \Magento\Catalog\Model\Category $childCategory */ - $childCategory = $this->objectManager->create(\Magento\Catalog\Model\Category::class); + /** @var Category $childCategory */ + $childCategory = $this->objectManager->create(Category::class); $childCategory->setName('Test Category 100') ->setParentId($parentCategoryId) ->setLevel(2) @@ -360,10 +461,10 @@ public function testCreateSubcategoryWithMultipleStores() protected function getCategoryByName($categoryName) { - /* @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ - - $collection = $this->objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); + /* @var Collection $collection */ + $collection = $this->objectManager->create(Collection::class); $collection->addNameToResult()->load(); + return $collection->getItemByColumnValue('name', $categoryName); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php index cb2f436ae90a0..58a7a0fbb2ace 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/FlatTest.php @@ -246,6 +246,7 @@ public function testDeleteCategory() * * @magentoConfigFixture current_store catalog/frontend/flat_catalog_category true * @magentoAppArea frontend + * @magentoDbIsolation disabled */ public function testFlatAfterDeleted() { @@ -348,19 +349,21 @@ private function instantiateCategoryModel() */ private function createSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $category = $this->getLoadedDefaultCategory(); - - $categoryOne = $this->instantiateCategoryModel(); - $categoryOne->setName('Category One')->setPath($category->getPath())->setIsActive(true); - $category->getResource()->save($categoryOne); - self::$categoryOne = $categoryOne->getId(); - - $categoryTwo = $this->instantiateCategoryModel(); - $categoryTwo->setName('Category Two')->setPath($categoryOne->getPath())->setIsActive(true); - $category->getResource()->save($categoryTwo); - self::$categoryTwo = $categoryTwo->getId(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $category = $this->getLoadedDefaultCategory(); + + $categoryOne = $this->instantiateCategoryModel(); + $categoryOne->setName('Category One')->setPath($category->getPath())->setIsActive(true); + $category->getResource()->save($categoryOne); + self::$categoryOne = $categoryOne->getId(); + + $categoryTwo = $this->instantiateCategoryModel(); + $categoryTwo->setName('Category Two')->setPath($categoryOne->getPath())->setIsActive(true); + $category->getResource()->save($categoryTwo); + self::$categoryTwo = $categoryTwo->getId(); + } + ); } /** @@ -371,11 +374,13 @@ private function createSubCategoriesInDefaultCategory() */ private function moveSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $this->createSubCategoriesInDefaultCategory(); - $categoryTwo = $this->getLoadedCategory(self::$categoryTwo); - $categoryTwo->move(self::$defaultCategoryId, self::$categoryOne); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $this->createSubCategoriesInDefaultCategory(); + $categoryTwo = $this->getLoadedCategory(self::$categoryTwo); + $categoryTwo->move(self::$defaultCategoryId, self::$categoryOne); + } + ); } /** @@ -386,10 +391,12 @@ private function moveSubCategoriesInDefaultCategory() */ private function deleteSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $this->createSubCategoriesInDefaultCategory(); - $this->removeSubCategoriesInDefaultCategory(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $this->createSubCategoriesInDefaultCategory(); + $this->removeSubCategoriesInDefaultCategory(); + } + ); } /** @@ -398,13 +405,15 @@ private function deleteSubCategoriesInDefaultCategory() */ private function removeSubCategoriesInDefaultCategory() { - $this->executeWithFlatEnabledInAdminArea(function () { - $category = $this->instantiateCategoryModel(); - $category->load(self::$categoryTwo); - $category->delete(); - $category->load(self::$categoryOne); - $category->delete(); - }); + $this->executeWithFlatEnabledInAdminArea( + function () { + $category = $this->instantiateCategoryModel(); + $category->load(self::$categoryTwo); + $category->delete(); + $category->load(self::$categoryOne); + $category->delete(); + } + ); } /** @@ -468,12 +477,4 @@ private function getActiveConfigInstance() \Magento\Framework\App\Config\MutableScopeConfigInterface::class ); } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php index d0a4f2ead4d6b..d1e040a307587 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Eav/Action/FullTest.php @@ -32,7 +32,7 @@ public static function setUpBeforeClass() protected function setUp() { - $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Eav\Processor::class ); } @@ -46,24 +46,24 @@ protected function setUp() public function testReindexAll() { /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attr **/ - $attr = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class) + $attr = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class) ->getAttribute('catalog_product', 'weight'); $attr->setIsFilterable(1)->save(); $this->assertTrue($attr->isIndexable()); - $priceIndexerProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $priceIndexerProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Price\Processor::class ); $priceIndexerProcessor->reindexAll(); $this->_processor->reindexAll(); - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Block\Product\ListProduct::class ); @@ -82,12 +82,4 @@ public function testReindexAll() $this->assertEquals(1, $product->getWeight()); } } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php index 677135092526c..095fa864ccb41 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/FullTest.php @@ -11,6 +11,8 @@ use Magento\Catalog\Model\Indexer\Product\Flat\Processor; use Magento\Catalog\Model\Indexer\Product\Flat\State; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; @@ -35,6 +37,22 @@ class FullTest extends \Magento\TestFramework\Indexer\TestCase */ private $objectManager; + /** + * @inheritdoc + */ + public static function setUpBeforeClass() + { + /* + * Due to insufficient search engine isolation for Elasticsearch, this class must explicitly perform + * a fulltext reindex prior to running its tests. + * + * This should be removed upon completing MC-19455. + */ + $indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + $fulltextIndexer = $indexRegistry->get(Fulltext::INDEXER_ID); + $fulltextIndexer->reindexAll(); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php index a6d1aa5be3e37..d2fb64813dd1e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/DataProvider/PriceTest.php @@ -81,6 +81,7 @@ public function getRangeItemCountsDataProvider() /** * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation disabled + * @magentoConfigFixture default/catalog/search/engine mysql * @dataProvider getRangeItemCountsDataProvider */ public function testGetRangeItemCounts($inputRange, $expectedItemCounts) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php new file mode 100644 index 0000000000000..94bbcd8bae66b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/CreateCustomOptionsTest.php @@ -0,0 +1,929 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Validator\Exception as ValidatorException; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test product custom options create. + * Testing option types: "Area", "File", "Drop-down", "Radio-Buttons", + * "Checkbox", "Multiple Select", "Date", "Date & Time" and "Time". + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class CreateCustomOptionsTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * Product repository. + * + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $optionRepository; + + /** + * Custom option factory. + * + * @var ProductCustomOptionInterfaceFactory + */ + private $customOptionFactory; + + /** + * @var ProductCustomOptionValuesInterfaceFactory + */ + private $customOptionValueFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->optionRepository = $this->objectManager->create(ProductCustomOptionRepositoryInterface::class); + $this->customOptionFactory = $this->objectManager->create(ProductCustomOptionInterfaceFactory::class); + $this->customOptionValueFactory = $this->objectManager + ->create(ProductCustomOptionValuesInterfaceFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * Test to save option price by store. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_options.php + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * + * @magentoConfigFixture default_store catalog/price/scope 1 + * @magentoConfigFixture secondstore_store catalog/price/scope 1 + */ + public function testSaveOptionPriceByStore(): void + { + $secondWebsitePrice = 22.0; + $currentStoreId = $this->storeManager->getStore()->getId(); + $customStoreId = $this->storeManager->getStore('secondstore')->getId(); + $product = $this->productRepository->get('simple'); + $option = $product->getOptions()[0]; + $defaultPrice = $option->getPrice(); + $option->setPrice($secondWebsitePrice); + $product->setStoreId($customStoreId); + // set Current store='secondstore' to correctly save product options for 'secondstore' + try { + $this->storeManager->setCurrentStore($customStoreId); + $this->productRepository->save($product); + } finally { + $this->storeManager->setCurrentStore($currentStoreId); + } + $product = $this->productRepository->get('simple', false, $currentStoreId, true); + $option = $product->getOptions()[0]; + $this->assertEquals($defaultPrice, $option->getPrice(), 'Price value by default store is wrong'); + $product = $this->productRepository->get('simple', false, $customStoreId, true); + $option = $product->getOptions()[0]; + $this->assertEquals($secondWebsitePrice, $option->getPrice(), 'Price value by custom store is wrong'); + } + + /** + * Test add to product custom options with text type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeTextDataProvider + * + * @param array $optionData + */ + public function testCreateOptionsWithTypeText(array $optionData): void + { + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $this->assertEquals($optionData['price'], $option->getPrice()); + $this->assertEquals($optionData['price_type'], $option->getPriceType()); + $this->assertEquals($optionData['sku'], $option->getSku()); + $maxCharacters = $optionData['max_characters'] ?? 0; + $this->assertEquals($maxCharacters, $option->getMaxCharacters()); + } + + /** + * Tests removing ineligible characters from file_extension. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider fileExtensionsDataProvider + * + * @param string $rawExtensions + * @param string $expectedExtensions + */ + public function testFileExtensions(string $rawExtensions, string $expectedExtensions): void + { + $product = $this->productRepository->get('simple'); + $optionData = [ + 'title' => 'file option', + 'type' => 'file', + 'is_require' => true, + 'sort_order' => 3, + 'price' => 30.0, + 'price_type' => 'percent', + 'sku' => 'sku3', + 'file_extension' => $rawExtensions, + 'image_size_x' => 10, + 'image_size_y' => 20, + ]; + $fileOption = $this->customOptionFactory->create(['data' => $optionData]); + $product->addOption($fileOption); + $this->productRepository->save($product); + $product = $this->productRepository->get('simple'); + $fileOption = $product->getOptions()[0]; + $actualExtensions = $fileOption->getFileExtension(); + $this->assertEquals($expectedExtensions, $actualExtensions); + } + + /** + * Test add to product custom options with select type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeSelectDataProvider + * + * @param array $optionData + * @param array $optionValueData + */ + public function testCreateOptionsWithTypeSelect(array $optionData, array $optionValueData): void + { + $optionValue = $this->customOptionValueFactory->create(['data' => $optionValueData]); + $optionData['values'] = [$optionValue]; + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $optionValues = $option->getValues(); + $this->assertCount(1, $optionValues); + $this->assertNotNull($optionValues); + $optionValue = reset($optionValues); + $this->assertEquals($optionValueData['title'], $optionValue->getTitle()); + $this->assertEquals($optionValueData['price'], $optionValue->getPrice()); + $this->assertEquals($optionValueData['price_type'], $optionValue->getPriceType()); + $this->assertEquals($optionValueData['sku'], $optionValue->getSku()); + $this->assertEquals($optionValueData['sort_order'], $optionValue->getSortOrder()); + } + + /** + * Test add to product custom options with date type. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsTypeDateDataProvider + * + * @param array $optionData + */ + public function testCreateOptionsWithTypeDate(array $optionData): void + { + $option = $this->baseCreateCustomOptionAndAssert($optionData); + $this->assertEquals($optionData['price'], $option->getPrice()); + $this->assertEquals($optionData['price_type'], $option->getPriceType()); + $this->assertEquals($optionData['sku'], $option->getSku()); + } + + /** + * Check that error throws if we save porduct with custom option without some field. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options.php + * + * @dataProvider productCustomOptionsWithErrorDataProvider + * + * @param array $optionData + * @param \Exception $expectedErrorObject + */ + public function testCreateOptionWithError(array $optionData, \Exception $expectedErrorObject): void + { + $product = $this->productRepository->get('simple'); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $product->setOptions([$createdOption]); + $this->expectExceptionObject($expectedErrorObject); + $this->productRepository->save($product); + } + + /** + * Add option to product with type text data provider. + * + * @return array + */ + public function productCustomOptionsTypeTextDataProvider(): array + { + return [ + 'area_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'area_field_options_with_max_charters_configuration' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'area_field_options_without_max_charters_configuration' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + ]; + } + + /** + * Data provider for testFileExtensions. + * + * @return array + */ + public function fileExtensionsDataProvider(): array + { + return [ + ['JPG, PNG, GIF', 'jpg, png, gif'], + ['jpg, jpg, jpg', 'jpg'], + ['jpg, png, gif', 'jpg, png, gif'], + ['jpg png gif', 'jpg, png, gif'], + ['!jpg@png#gif%', 'jpg, png, gif'], + ['jpg, png, 123', 'jpg, png, 123'], + ['', ''], + ]; + } + + /** + * Add option to product with type text data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsTypeSelectDataProvider(): array + { + return [ + 'drop_down_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'drop_down_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'drop_down', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'radio_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'radio', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'checkbox_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'checkbox', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_not_required_option' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_option_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + 'multiple_field_option_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'title' => 'Test option 1', + 'type' => 'multiple', + 'price_type' => 'fixed', + ], + [ + 'record_id' => 0, + 'title' => 'Test option 1 value 1', + 'price' => 10, + 'price_type' => 'percent', + 'sku' => 'test-option-1-value-1', + 'sort_order' => 1, + ], + ], + ]; + } + + /** + * Add option to product with type text data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsTypeDateDataProvider(): array + { + return [ + 'date_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'date_time_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'date_time_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'date_time', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + 'time_field_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_not_required_options' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 0, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_options_with_fixed_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'fixed', + ], + ], + 'time_field_options_with_percent_price' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => 'time', + 'price' => 10, + 'price_type' => 'percent', + ], + ], + ]; + } + + /** + * Add option to product for get option save error data provider. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @return array + */ + public function productCustomOptionsWithErrorDataProvider(): array + { + return [ + 'error_option_without_product_sku' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + ], + new CouldNotSaveException(__('The ProductSku is empty. Set the ProductSku and try again.')), + ], + 'error_option_without_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__("Missed values for option required fields\nInvalid option type")), + ], + 'error_option_wrong_price_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'test_wrong_price_type', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_price_type' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price' => 10, + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_price_value' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => 'Test option title 1', + 'type' => 'area', + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Invalid option value')), + ], + 'error_option_without_title' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Missed values for option required fields')), + ], + 'error_option_with_empty_title' => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 50, + 'title' => '', + 'type' => 'area', + 'price' => 10, + 'price_type' => 'fixed', + 'product_sku' => 'simple', + ], + new ValidatorException(__('Missed values for option required fields')), + ], + ]; + } + + /** + * Create custom option and save product with created option, check base assertions. + * + * @param array $optionData + * @return ProductCustomOptionInterface + */ + private function baseCreateCustomOptionAndAssert(array $optionData): ProductCustomOptionInterface + { + $product = $this->productRepository->get('simple'); + $createdOption = $this->customOptionFactory->create(['data' => $optionData]); + $createdOption->setProductSku($product->getSku()); + $product->setOptions([$createdOption]); + $this->productRepository->save($product); + $productCustomOptions = $this->optionRepository->getProductOptions($product); + $this->assertCount(1, $productCustomOptions); + $option = reset($productCustomOptions); + $this->assertEquals($optionData['title'], $option->getTitle()); + $this->assertEquals($optionData['type'], $option->getType()); + $this->assertEquals($optionData['is_require'], $option->getIsRequire()); + + return $option; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php index 7421402455b28..03455bb341cae 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/CreateHandlerTest.php @@ -3,50 +3,87 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Gallery; -use Magento\Framework\Exception\FileSystemException; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; /** - * Test class for \Magento\Catalog\Model\Product\Gallery\CreateHandler. + * Provides tests for media gallery images creation during product save. * * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @magentoDbIsolation enabled */ class CreateHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Gallery\CreateHandler + * @var string */ - protected $createHandler; - private $fileName = '/m/a/magento_image.jpg'; + /** + * @var string + */ private $fileLabel = 'Magento image'; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CreateHandler + */ + private $createHandler; + + /** + * @var Gallery + */ + private $galleryResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ProductResource + */ + private $productResource; + + /** + * @inheritdoc + */ protected function setUp() { - $this->createHandler = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product\Gallery\CreateHandler::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->createHandler = $this->objectManager->create(CreateHandler::class); + $this->galleryResource = $this->objectManager->create(Gallery::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); } /** + * Tests gallery processing on product duplication. + * * @covers \Magento\Catalog\Model\Product\Gallery\CreateHandler::execute + * + * @return void */ - public function testExecuteWithImageDuplicate() + public function testExecuteWithImageDuplicate(): void { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => $this->fileLabel]]] - ); - $product->setData('image', $this->fileName); + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => $this->fileLabel]]], + 'image' => $this->fileName, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); $this->assertEquals($this->fileLabel, $product->getData('image_label')); @@ -62,39 +99,29 @@ public function testExecuteWithImageDuplicate() } /** - * Check sanity of posted image file name + * Check sanity of posted image file name. * * @param string $imageFileName - * @throws FileSystemException * @expectedException \Magento\Framework\Exception\FileSystemException + * @expectedExceptionMessageRegExp ".+ file doesn't exist." + * @expectedExceptionMessageRegExp "/^((?!\.\.\/).)*$/" * @dataProvider illegalFilenameDataProvider + * @return void */ - public function testExecuteWithIllegalFilename($imageFileName) + public function testExecuteWithIllegalFilename(string $imageFileName): void { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $imageFileName, 'label' => 'New image']]] - ); + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $imageFileName, 'label' => 'New image']]], + ]; + $product = $this->initProduct($data); $product->setData('image', $imageFileName); - - try { - $this->createHandler->execute($product); - } catch (FileSystemException $exception) { - $this->assertContains(" file doesn't exist.", $exception->getLogMessage()); - $this->assertNotContains('../', $exception->getLogMessage()); - throw $exception; - } + $this->createHandler->execute($product); } /** * @return array */ - public function illegalFilenameDataProvider() + public function illegalFilenameDataProvider(): array { return [ ['../../../../../.htaccess'], @@ -103,123 +130,170 @@ public function illegalFilenameDataProvider() } /** + * Tests gallery processing with different image roles. + * * @dataProvider executeDataProvider - * @param $image - * @param $smallImage - * @param $swatchImage - * @param $thumbnail + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void */ - public function testExecuteWithImageRoles($image, $smallImage, $swatchImage, $thumbnail) - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]] - ); - $product->setData('image', $image); - $product->setData('small_image', $smallImage); - $product->setData('swatch_image', $swatchImage); - $product->setData('thumbnail', $thumbnail); + public function testExecuteWithImageRoles( + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]], + 'image' => $image, + 'small_image' => $smallImage, + 'swatch_image' => $swatchImage, + 'thumbnail' => $thumbnail, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); - - $resource = $product->getResource(); - $id = $product->getId(); - $storeId = $product->getStoreId(); - - $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); - $this->assertEquals( - $image, - $resource->getAttributeRawValue($id, $resource->getAttribute('image'), $storeId) - ); - $this->assertEquals( - $smallImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('small_image'), $storeId) - ); - $this->assertEquals( - $swatchImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('swatch_image'), $storeId) - ); - $this->assertEquals( - $thumbnail, - $resource->getAttributeRawValue($id, $resource->getAttribute('thumbnail'), $storeId) - ); + $this->assertMediaImageRoleAttributes($product, $image, $smallImage, $swatchImage, $thumbnail); } /** + * Tests gallery processing without images. + * * @dataProvider executeDataProvider - * @param $image - * @param $smallImage - * @param $swatchImage - * @param $thumbnail + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void */ - public function testExecuteWithoutImages($image, $smallImage, $swatchImage, $thumbnail) - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load(1); - $product->setData( - 'media_gallery', - ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]] - ); - $product->setData('image', $image); - $product->setData('small_image', $smallImage); - $product->setData('swatch_image', $swatchImage); - $product->setData('thumbnail', $thumbnail); + public function testExecuteWithoutImages( + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, 'label' => '']]], + 'image' => $image, + 'small_image' => $smallImage, + 'swatch_image' => $swatchImage, + 'thumbnail' => $thumbnail, + ]; + $product = $this->initProduct($data); $this->createHandler->execute($product); - $product->unsetData('image'); $product->unsetData('small_image'); $product->unsetData('swatch_image'); $product->unsetData('thumbnail'); $this->createHandler->execute($product); - - $resource = $product->getResource(); - $id = $product->getId(); - $storeId = $product->getStoreId(); - - $this->assertStringStartsWith('/m/a/magento_image', $product->getData('media_gallery/images/image/new_file')); - $this->assertEquals( - $image, - $resource->getAttributeRawValue($id, $resource->getAttribute('image'), $storeId) - ); - $this->assertEquals( - $smallImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('small_image'), $storeId) - ); - $this->assertEquals( - $swatchImage, - $resource->getAttributeRawValue($id, $resource->getAttribute('swatch_image'), $storeId) - ); - $this->assertEquals( - $thumbnail, - $resource->getAttributeRawValue($id, $resource->getAttribute('thumbnail'), $storeId) - ); + $this->assertMediaImageRoleAttributes($product, $image, $smallImage, $swatchImage, $thumbnail); } /** * @return array */ - public function executeDataProvider() + public function executeDataProvider(): array { return [ [ 'image' => $this->fileName, 'small_image' => $this->fileName, 'swatch_image' => $this->fileName, - 'thumbnail' => $this->fileName + 'thumbnail' => $this->fileName, ], [ 'image' => 'no_selection', 'small_image' => 'no_selection', 'swatch_image' => 'no_selection', - 'thumbnail' => 'no_selection' - ] + 'thumbnail' => 'no_selection', + ], + ]; + } + + /** + * Tests gallery processing with variations of additional gallery image fields. + * + * @dataProvider additionalGalleryFieldsProvider + * @param string $mediaField + * @param string $value + * @param string|null $expectedValue + * @return void + */ + public function testExecuteWithAdditionalGalleryFields( + string $mediaField, + string $value, + ?string $expectedValue + ): void { + $data = [ + 'media_gallery' => ['images' => ['image' => ['file' => $this->fileName, $mediaField => $value]]], ]; + $product = $this->initProduct($data); + $this->createHandler->execute($product); + $galleryAttributeId = $this->productResource->getAttribute('media_gallery')->getAttributeId(); + $productImages = $this->galleryResource->loadProductGalleryByAttributeId($product, $galleryAttributeId); + $image = reset($productImages); + $this->assertEquals($image[$mediaField], $expectedValue); + } + + /** + * @return array + */ + public function additionalGalleryFieldsProvider(): array + { + return [ + ['label', '', null], + ['label', 'Some label', 'Some label'], + ['disabled', '0', '0'], + ['disabled', '1', '1'], + ['position', '1', '1'], + ['position', '2', '2'], + ]; + } + + /** + * Returns product for testing. + * + * @param array $data + * @return Product + */ + private function initProduct(array $data): Product + { + $product = $this->productRepository->getById(1); + $product->addData($data); + + return $product; + } + + /** + * Asserts product attributes related to gallery images. + * + * @param Product $product + * @param string $image + * @param string $smallImage + * @param string $swatchImage + * @param string $thumbnail + * @return void + */ + private function assertMediaImageRoleAttributes( + Product $product, + string $image, + string $smallImage, + string $swatchImage, + string $thumbnail + ): void { + $productsImageData = $this->productResource->getAttributeRawValue( + $product->getId(), + ['image', 'small_image', 'thumbnail', 'swatch_image'], + $product->getStoreId() + ); + $this->assertStringStartsWith( + '/m/a/magento_image', + $product->getData('media_gallery/images/image/new_file') + ); + $this->assertEquals($image, $productsImageData['image']); + $this->assertEquals($smallImage, $productsImageData['small_image']); + $this->assertEquals($swatchImage, $productsImageData['swatch_image']); + $this->assertEquals($thumbnail, $productsImageData['thumbnail']); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php index a6538423f37a1..8463577c34ed9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/DateTest.php @@ -6,13 +6,16 @@ namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\DataObject; + /** - * Test for \Magento\Catalog\Model\Product\Option\Type\Date + * Test for customizable product option with "Date" type */ class DateTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Product\Option\Type\Date + * @var Date */ protected $model; @@ -28,12 +31,13 @@ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->model = $this->objectManager->create( - \Magento\Catalog\Model\Product\Option\Type\Date::class + Date::class ); } /** - * @covers \Magento\Catalog\Model\Product\Option\Type\Date::prepareOptionValueForRequest() + * Check if option value for request is the same as expected + * * @dataProvider prepareOptionValueForRequestDataProvider * @param array $optionValue * @param array $infoBuyRequest @@ -54,10 +58,10 @@ public function testPrepareOptionValueForRequest( /** @var \Magento\Quote\Model\Quote\Item $item */ $item = $this->objectManager->create(\Magento\Quote\Model\Quote\Item::class); $item->addOption($option); - /** @var \Magento\Catalog\Model\Product\Option|null $productOption */ + /** @var Option|null $productOption */ $productOption = $productOptionData ? $this->objectManager->create( - \Magento\Catalog\Model\Product\Option::class, + Option::class, ['data' => $productOptionData] ) : null; @@ -69,6 +73,8 @@ public function testPrepareOptionValueForRequest( } /** + * Data provider for testPrepareOptionValueForRequest + * * @return array */ public function prepareOptionValueForRequestDataProvider() @@ -109,4 +115,76 @@ public function prepareOptionValueForRequestDataProvider() ], ]; } + + /** + * Check date in prepareForCart method with javascript calendar and Asia/Singapore timezone + * + * @dataProvider testPrepareForCartDataProvider + * @param array $dateData + * @param array $productOptionData + * @param array $requestData + * @param string $expectedOptionValueForRequest + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoConfigFixture current_store general/locale/timezone Asia/Singapore + */ + public function testPrepareForCart( + array $dateData, + array $productOptionData, + array $requestData, + string $expectedOptionValueForRequest + ) { + $this->model->setData($dateData); + /** @var Option|null $productOption */ + $productOption = $productOptionData + ? $this->objectManager->create( + Option::class, + ['data' => $productOptionData] + ) + : null; + $this->model->setOption($productOption); + $request = new DataObject(); + $request->setData($requestData); + $this->model->setRequest($request); + $actualOptionValueForRequest = $this->model->prepareForCart(); + $this->assertSame($expectedOptionValueForRequest, $actualOptionValueForRequest); + } + + /** + * Data provider for testPrepareForCart + * + * @return array + */ + public function testPrepareForCartDataProvider() + { + return [ + [ + // $dateData + [ + 'is_valid' => true, + 'user_value' => [ + 'date' => '09/30/2019', + 'year' => 0, + 'month' => 0, + 'day' => 0, + 'hour' => 0, + 'minute' => 0, + 'day_part' => '', + 'date_internal' => '' + ] + ], + // $productOptionData + ['id' => '11', 'value' => '{"qty":12}', 'type' => 'date'], + // $requestData + [ + 'options' => [ + [ + 'date' => '09/30/2019' + ] + ] + ], + // $expectedOptionValueForRequest + '2019-09-30 00:00:00' + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php deleted file mode 100644 index bfb49686447c0..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/OptionTest.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Model\Product; - -use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use Magento\TestFramework\Helper\Bootstrap; - -/** - * @magentoAppArea adminhtml - * @magentoAppIsolation enabled - * @magentoDbIsolation enabled - */ -class OptionTest extends \PHPUnit\Framework\TestCase -{ - /** - * Product repository. - * - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * Custom option factory. - * - * @var ProductCustomOptionInterfaceFactory - */ - private $customOptionFactory; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - $this->customOptionFactory = Bootstrap::getObjectManager()->create(ProductCustomOptionInterfaceFactory::class); - $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); - } - - /** - * Tests removing ineligible characters from file_extension. - * - * @param string $rawExtensions - * @param string $expectedExtensions - * @dataProvider fileExtensionsDataProvider - * @magentoDataFixture Magento/Catalog/_files/product_without_options.php - */ - public function testFileExtensions(string $rawExtensions, string $expectedExtensions) - { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->get('simple'); - /** @var \Magento\Catalog\Model\Product\Option $fileOption */ - $fileOption = $this->createFileOption($rawExtensions); - $product->addOption($fileOption); - $product->save(); - $product = $this->productRepository->get('simple'); - $fileOption = $product->getOptions()[0]; - $actualExtensions = $fileOption->getFileExtension(); - $this->assertEquals($expectedExtensions, $actualExtensions); - } - - /** - * Data provider for testFileExtensions. - * - * @return array - */ - public function fileExtensionsDataProvider() - { - return [ - ['JPG, PNG, GIF', 'jpg, png, gif'], - ['jpg, jpg, jpg', 'jpg'], - ['jpg, png, gif', 'jpg, png, gif'], - ['jpg png gif', 'jpg, png, gif'], - ['!jpg@png#gif%', 'jpg, png, gif'], - ['jpg, png, 123', 'jpg, png, 123'], - ['', ''], - ]; - } - - /** - * Create file type option for product. - * - * @param string $rawExtensions - * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface|void - */ - private function createFileOption(string $rawExtensions) - { - $data = [ - 'title' => 'file option', - 'type' => 'file', - 'is_require' => true, - 'sort_order' => 3, - 'price' => 30.0, - 'price_type' => 'percent', - 'sku' => 'sku3', - 'file_extension' => $rawExtensions, - 'image_size_x' => 10, - 'image_size_y' => 20, - ]; - - return $this->customOptionFactory->create(['data' => $data]); - } - - /** - * Test to save option price by store - * - * @magentoDataFixture Magento/Catalog/_files/product_with_options.php - * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php - * @magentoConfigFixture default_store catalog/price/scope 1 - * @magentoConfigFixture secondstore_store catalog/price/scope 1 - */ - public function testSaveOptionPriceByStore() - { - $secondWebsitePrice = 22.0; - $defaultStoreId = $this->storeManager->getStore()->getId(); - $secondStoreId = $this->storeManager->getStore('secondstore')->getId(); - - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->get('simple'); - $option = $product->getOptions()[0]; - $defaultPrice = $option->getPrice(); - - $option->setPrice($secondWebsitePrice); - $product->setStoreId($secondStoreId); - // set Current store='secondstore' to correctly save product options for 'secondstore' - $this->storeManager->setCurrentStore($secondStoreId); - $this->productRepository->save($product); - $this->storeManager->setCurrentStore($defaultStoreId); - - $product = $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID, true); - $option = $product->getOptions()[0]; - $this->assertEquals($defaultPrice, $option->getPrice(), 'Price value by default store is wrong'); - - $product = $this->productRepository->get('simple', false, $secondStoreId, true); - $option = $product->getOptions()[0]; - $this->assertEquals($secondWebsitePrice, $option->getPrice(), 'Price value by store_id=1 is wrong'); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php index 4cf059d4bf692..e0860897fcc24 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php @@ -5,11 +5,15 @@ */ namespace Magento\Catalog\Model\Product; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; + /** * Test class for \Magento\Catalog\Model\Product\Url. * * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UrlTest extends \PHPUnit\Framework\TestCase { @@ -49,6 +53,7 @@ public function testGetUrlInStore() * @magentoConfigFixture fixturestore_store web/unsecure/base_url http://sample-second.com/ * @magentoConfigFixture fixturestore_store web/unsecure/base_link_url http://sample-second.com/ * @magentoDataFixture Magento/Catalog/_files/product_simple_multistore.php + * @magentoDbIsolation disabled * @dataProvider getUrlsWithSecondStoreProvider * @magentoAppArea adminhtml */ @@ -132,4 +137,48 @@ public function testGetUrl() $product->setId(100); $this->assertContains('catalog/product/view/id/100/', $this->_model->getUrl($product)); } + + /** + * Check that rearranging product url rewrites do not influence on whether to use category in product links + * + * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 + */ + public function testGetProductUrlWithRearrangedUrlRewrites() + { + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class + ); + $categoryRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\CategoryRepository::class + ); + $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Registry::class + ); + $urlFinder = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\UrlRewrite\Model\UrlFinderInterface::class + ); + $urlPersist = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\UrlRewrite\Model\UrlPersistInterface::class + ); + + $product = $productRepository->get('simple'); + $category = $categoryRepository->get($product->getCategoryIds()[0]); + $registry->register('current_category', $category); + $this->assertNotContains($category->getUrlPath(), $this->_model->getProductUrl($product)); + + $rewrites = $urlFinder->findAllByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE + ] + ); + $this->assertGreaterThan(1, count($rewrites)); + foreach ($rewrites as $rewrite) { + if ($rewrite->getRequestPath() === 'simple-product.html') { + $rewrite->setUrlRewriteId($rewrite->getUrlRewriteId() + 1000); + } + } + $urlPersist->replace($rewrites); + $this->assertNotContains($category->getUrlPath(), $this->_model->getProductUrl($product)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php new file mode 100644 index 0000000000000..9481702183327 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\HydratorInterface; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test Product Hydrator + */ +class ProductHydratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Bootstrap + */ + private $objectManager; + + /** + * @var HydratorPool + */ + private $hydratorPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->hydratorPool = $this->objectManager->create(HydratorPool::class); + } + + /** + * Test that Hydrator correctly populates entity with data + */ + public function testProductHydrator() + { + $addAttributes = [ + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + ]; + + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->setId(42) + ->setSku('product') + ->setName('Product') + ->setPrice(10) + ->setQty(123); + $product->lockAttribute('sku'); + $product->lockAttribute('type_id'); + $product->lockAttribute('price'); + + /** @var HydratorInterface $hydrator */ + $hydrator = $this->hydratorPool->getHydrator(ProductInterface::class); + $hydrator->hydrate($product, $addAttributes); + + $expected = [ + 'entity_id' => 42, + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + 'price' => 10, + 'qty' => 123, + ]; + $this->assertEquals($expected, $product->getData()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index c34120404a950..d7da47ef78724 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -8,7 +8,12 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\Product; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; /** * Tests product model: @@ -26,31 +31,32 @@ class ProductTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; /** - * @var \Magento\Catalog\Model\Product + * @var Product */ protected $_model; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @inheritdoc */ protected function setUp() { - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->_model = $this->objectManager->create(Product::class); } /** - * @throws \Magento\Framework\Exception\FileSystemException - * @return void + * @inheritdoc */ public static function tearDownAfterClass() { @@ -74,6 +80,8 @@ public static function tearDownAfterClass() } /** + * Test can affect options + * * @return void */ public function testCanAffectOptions() @@ -84,6 +92,8 @@ public function testCanAffectOptions() } /** + * Test CRUD + * * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml @@ -116,6 +126,8 @@ public function testCRUD() } /** + * Test clean cache + * * @return void */ public function testCleanCache() @@ -139,6 +151,8 @@ public function testCleanCache() } /** + * Test add image to media gallery + * * @return void */ public function testAddImageToMediaGallery() @@ -183,6 +197,8 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) } /** + * Test duplicate method + * * @magentoAppIsolation enabled * @magentoAppArea adminhtml */ @@ -213,6 +229,8 @@ public function testDuplicate() } /** + * Test duplicate sku generation + * * @magentoAppArea adminhtml */ public function testDuplicateSkuGeneration() @@ -244,6 +262,8 @@ protected function _undo($duplicate) } /** + * Test visibility api + * * @covers \Magento\Catalog\Model\Product::getVisibleInCatalogStatuses * @covers \Magento\Catalog\Model\Product::getVisibleStatuses * @covers \Magento\Catalog\Model\Product::isVisibleInCatalog @@ -286,6 +306,8 @@ public function testVisibilityApi() } /** + * Test isDuplicable and setIsDuplicable methods + * * @covers \Magento\Catalog\Model\Product::isDuplicable * @covers \Magento\Catalog\Model\Product::setIsDuplicable */ @@ -297,6 +319,8 @@ public function testIsDuplicable() } /** + * Test isSalable, isSaleable, isAvailable and isInStock methods + * * @covers \Magento\Catalog\Model\Product::isSalable * @covers \Magento\Catalog\Model\Product::isSaleable * @covers \Magento\Catalog\Model\Product::isAvailable @@ -314,6 +338,8 @@ public function testIsSalable() } /** + * Test isSalable method when Status is disabled + * * @covers \Magento\Catalog\Model\Product::isSalable * @covers \Magento\Catalog\Model\Product::isSaleable * @covers \Magento\Catalog\Model\Product::isAvailable @@ -331,6 +357,8 @@ public function testIsNotSalableWhenStatusDisabled() } /** + * Test isVirtual and getIsVirtual methods + * * @covers \Magento\Catalog\Model\Product::isVirtual * @covers \Magento\Catalog\Model\Product::getIsVirtual */ @@ -349,6 +377,8 @@ public function testIsVirtual() } /** + * Test toArray method + * * @return void */ public function testToArray() @@ -359,6 +389,8 @@ public function testToArray() } /** + * Test fromArray method + * * @return void */ public function testFromArray() @@ -368,6 +400,8 @@ public function testFromArray() } /** + * Test set original data backend + * * @magentoAppArea adminhtml */ public function testSetOrigDataBackend() @@ -378,6 +412,8 @@ public function testSetOrigDataBackend() } /** + * Test reset method + * * @magentoAppArea frontend */ public function testReset() @@ -418,6 +454,8 @@ protected function _assertEmpty($model) } /** + * Test is products has sku + * * @magentoDataFixture Magento/Catalog/_files/multiple_products.php */ public function testIsProductsHasSku() @@ -433,6 +471,8 @@ public function testIsProductsHasSku() } /** + * Test process by request + * * @return void */ public function testProcessBuyRequest() @@ -444,6 +484,8 @@ public function testProcessBuyRequest() } /** + * Test validate method + * * @return void */ public function testValidate() @@ -480,6 +522,8 @@ public function testValidate() } /** + * Test validate unique input attribute value + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/products_with_unique_input_attribute.php */ @@ -523,6 +567,8 @@ public function testValidateUniqueInputAttributeValue() } /** + * Test validate unique input attribute value on the same product + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/products_with_unique_input_attribute.php */ @@ -618,8 +664,53 @@ public function testSaveWithBackordersEnabled(int $qty, int $stockStatus, bool $ $this->assertEquals($expectedStockStatus, $stockItem->getIsInStock()); } + /** + * Checking enable/disable product when Catalog Flat Product is enabled + * + * @magentoAppArea frontend + * @magentoDbIsolation disabled + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException + */ + public function testProductStatusWhenCatalogFlatProductIsEnabled() + { + // check if product flat table is enabled + $productFlatState = $this->objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\State::class); + $this->assertTrue($productFlatState->isFlatEnabled()); + // run reindex to create product flat table + $productFlatProcessor = $this->objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\Processor::class); + $productFlatProcessor->reindexAll(); + // get created simple product + $product = $this->productRepository->get('simple'); + // get db connection and the product flat table name + $resource = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = $resource->getConnection(); + $productFlatTableName = $productFlatState->getFlatIndexerHelper()->getFlatTableName(1); + // generate sql query to find created simple product in the flat table + $sql = $connection->select()->from($productFlatTableName)->where('sku =?', $product->getSku()); + // check if the product exists in the product flat table + $products = $connection->fetchAll($sql); + $this->assertEquals(Status::STATUS_ENABLED, $product->getStatus()); + $this->assertNotEmpty($products); + // disable product + $product->setStatus(Status::STATUS_DISABLED); + $product = $this->productRepository->save($product); + // check if the product exists in the product flat table + $products = $connection->fetchAll($sql); + $this->assertEquals(Status::STATUS_DISABLED, $product->getStatus()); + $this->assertEmpty($products); + } + /** * DataProvider for the testSaveWithBackordersEnabled() + * * @return array */ public function productWithBackordersDataProvider(): array diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php index 4cc6265a992fa..de0e881474cf0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php @@ -3,10 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + /** * Collection test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CollectionTest extends \PHPUnit\Framework\TestCase { @@ -31,15 +41,15 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->collection = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Collection::class ); - $this->processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->processor = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\Indexer\Product\Price\Processor::class ); - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->productRepository = Bootstrap::getObjectManager()->create( \Magento\Catalog\Api\ProductRepositoryInterface::class ); } @@ -54,7 +64,7 @@ public function testAddPriceDataOnSchedule() $this->processor->getIndexer()->setScheduled(true); $this->assertTrue($this->processor->getIndexer()->isScheduled()); - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get('simple'); @@ -73,7 +83,7 @@ public function testAddPriceDataOnSchedule() //reindexing $this->processor->getIndexer()->reindexList([1]); - $this->collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->collection = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Product\Collection::class ); $this->collection->addPriceData(0, 1); @@ -89,6 +99,69 @@ public function testAddPriceDataOnSchedule() $this->processor->getIndexer()->setScheduled(false); } + /** + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetVisibility() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); + $this->collection->setVisibility([Visibility::VISIBILITY_BOTH]); + $this->collection->load(); + /** @var \Magento\Catalog\Api\Data\ProductInterface[] $product */ + $items = $this->collection->getItems(); + $this->assertCount(2, $items); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetCategoryWithStoreFilter() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + + $category = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Category::class + )->load(333); + $this->collection->addCategoryFilter($category)->addStoreFilter(1); + $this->collection->load(); + + $collectionStoreFilterAfter = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory::class + )->create(); + $collectionStoreFilterAfter->addStoreFilter(1)->addCategoryFilter($category); + $collectionStoreFilterAfter->load(); + $this->assertEquals($this->collection->getItems(), $collectionStoreFilterAfter->getItems()); + $this->assertCount(1, $collectionStoreFilterAfter->getItems()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/categories.php + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ + public function testSetCategoryFilter() + { + $appState = Bootstrap::getObjectManager() + ->create(State::class); + $appState->setAreaCode(Area::AREA_CRONTAB); + + $category = \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Catalog\Model\Category::class + )->load(3); + $this->collection->addCategoryFilter($category); + $this->collection->load(); + $this->assertEquals($this->collection->getSize(), 3); + } + /** * @magentoDataFixture Magento/Catalog/_files/products.php * @magentoAppIsolation enabled @@ -98,7 +171,7 @@ public function testAddPriceDataOnSave() { $this->processor->getIndexer()->setScheduled(false); $this->assertFalse($this->processor->getIndexer()->isScheduled()); - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get('simple'); @@ -184,7 +257,7 @@ public function testJoinTable() $productTable = $this->collection->getTable('catalog_product_entity'); $urlRewriteTable = $this->collection->getTable('url_rewrite'); - // phpcs:ignore + // phpcs:ignore Magento2.SQL.RawQuery $expected = 'SELECT `e`.*, `alias`.`request_path` FROM `' . $productTable . '` AS `e`' . ' LEFT JOIN `' . $urlRewriteTable . '` AS `alias` ON (alias.entity_id =e.entity_id)' . ' AND (alias.entity_type = \'product\')'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php index 5cf6d00fe77ea..78ae21b1441bd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php @@ -7,12 +7,17 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\_files\MultiselectSourceMock; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; /** * Class SourceTest * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SourceTest extends \PHPUnit\Framework\TestCase { @@ -159,6 +164,42 @@ public function testReindexMultiselectAttribute() $this->assertCount(3, $result); } + /** + * Test for indexing product attribute without "all store view" value + * + * @magentoDataFixture Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view.php + * @magentoDbIsolation disabled + */ + public function testReindexSelectAttributeWithoutDefault() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var StoreInterface $store */ + $store = $objectManager->get(StoreManagerInterface::class) + ->getStore(); + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute **/ + $attribute = $objectManager->get(\Magento\Eav\Model\Config::class) + ->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); + /** @var AttributeOptionInterface $option */ + $option = $attribute->getOptions()[1]; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get('test_attribute_dropdown_without_default', false, 1); + $expected = [ + 'entity_id' => $product->getId(), + 'attribute_id' => $attribute->getId(), + 'store_id' => $store->getId(), + 'value' => $option->getValue(), + 'source_id' => $product->getId(), + ]; + $connection = $this->productResource->getConnection(); + $select = $connection->select()->from($this->productResource->getTable('catalog_product_index_eav')) + ->where('entity_id = ?', $product->getId()) + ->where('attribute_id = ?', $attribute->getId()); + + $result = $connection->fetchRow($select); + $this->assertEquals($expected, $result); + } + /** * @magentoDataFixture Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php * @magentoDbIsolation disabled diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php new file mode 100644 index 0000000000000..d48578aa73465 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$attributeSet = $objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); +$defaultSetId = $objectManager->create(\Magento\Catalog\Model\Product::class)->getDefaultAttributeSetid(); +$data = [ + 'attribute_set_name' => 'new_attribute_set', + 'entity_type_id' => $entityType->getId(), + 'sort_order' => 300, +]; + +$attributeSet->setData($data); +$attributeSet->validate(); +$attributeSet->save(); +$attributeSet->initFromSkeleton($defaultSetId); +$attributeSet->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php new file mode 100644 index 0000000000000..4cd18f6d23e8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_based_on_default_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); + +$attributeSetCollection = $objectManager->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class +)->create(); +$attributeSetCollection->addFilter('attribute_set_name', 'new_attribute_set'); +$attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); +$attributeSetCollection->setOrder('attribute_set_id'); +$attributeSetCollection->setPageSize(1); +$attributeSetCollection->load(); + +$attributeSet = $attributeSetCollection->fetchItem(); +$attributeSet->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index a5ab961932461..25bb55ffbc32c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -64,6 +64,7 @@ ->setIsActive(true) ->setIsAnchor(true) ->setPosition(1) + ->setDescription('Category 1.1 description.') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); @@ -79,6 +80,7 @@ ->setPosition(1) ->setCustomUseParentSettings(0) ->setCustomDesign('Magento/blank') + ->setDescription('This is the description for Category 1.1.1') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif new file mode 100755 index 0000000000000..82bccf7ba2fa6 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image.gif differ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php new file mode 100644 index 0000000000000..6ae4af61bc51a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; + +Bootstrap::getInstance()->reinitialize(); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 0, + 'is_qty_decimal' => 0, + 'is_in_stock' => 0, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php new file mode 100644 index 0000000000000..4c858f322f8a7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +include __DIR__ . '/product_simple_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php index 23fd8d7fe324e..928c036e8fb40 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php @@ -41,7 +41,7 @@ $productRepository->save($product); } catch (\Exception $e) { // problems during save -}; +} /** @var ProductInterface $product */ $product = $objectManager->create(ProductInterface::class); @@ -60,4 +60,4 @@ $productRepository->save($product); } catch (\Exception $e) { // problems during save -}; +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php new file mode 100644 index 0000000000000..4176e14209edb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; + +require __DIR__ . '/product_image.php'; +require __DIR__ . '/product_simple.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +$imageData = [ + 'file' => '/m/a/magento_image.jpg', + 'position' => 1, + 'label' => 'Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' +]; + +/** @var $product Product */ +$product->setStoreId(0) + ->setData('media_gallery', ['images' => [$imageData]]) + ->setCanSaveCustomOptions(true) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php new file mode 100644 index 0000000000000..2033f092b3979 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_image_without_types_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/product_simple_rollback.php'; +require __DIR__ . '/product_image_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view.php new file mode 100644 index 0000000000000..7c872a66f60d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var StoreInterface $store */ +$store = $storeManager->getStore(); +$eavConfig = $objectManager->get(EavConfig::class); +$eavConfig->clear(); +$attribute = $eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); +/** @var CategorySetup $installer */ +$installer = $objectManager->get(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE, 'Default'); + +/** @var ProductInterface $product */ +$product = $objectManager->get(ProductInterface::class); +$product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setName('Simple Product1') + ->setSku('test_attribute_dropdown_without_default') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); + +if (!$attribute->getId()) { + /** @var $attribute */ + $attribute = $objectManager->get(EavAttribute::class); + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); + $attribute->setData( + [ + 'attribute_code' => 'dropdown_without_default', + 'entity_type_id' => $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE), + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + $attributeRepository->save($attribute); + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'Default', + 'General', + $attribute->getId() + ); +} +/** @var AttributeOptionManagementInterface $options */ +$attributeOption = $objectManager->get(AttributeOptionManagementInterface::class); +/* Getting the first nonempty option */ +/** @var AttributeOptionInterface $option */ +$option = $attributeOption->getItems($attribute->getEntityTypeId(), $attribute->getAttributeCode())[1]; +$product->setStoreId($store->getId()) + ->setData('dropdown_without_default', $option->getValue()); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php new file mode 100644 index 0000000000000..a60588c16ab62 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_without_all_store_view_rollback.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Eav\Model\Config as EavConfig; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\Indexer\Product\Eav as ProductEav; +use Magento\Framework\Registry; + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = $objectManager->get(EavConfig::class); +$eavConfig->clear(); +/** @var AttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + /** @var AttributeInterface $attribute */ + $attribute = $attributeRepository->get(ProductAttributeInterface::ENTITY_TYPE_CODE, 'dropdown_without_default'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { + //Attribute already deleted +} +try { + /** @var ProductInterface $product */ + $product = $productRepository->get('test_attribute_dropdown_without_default'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already deleted +} +$objectManager->get(ProductEav::class)->executeRow($product->getId()); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 1b33cd695d06e..553242f0daec6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -24,10 +24,10 @@ use Magento\Framework\Filesystem; use Magento\Framework\Registry; use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\Source\Csv; use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; use Psr\Log\LoggerInterface; -use Magento\ImportExport\Model\Import\Source\Csv; /** * Class ProductTest @@ -1744,7 +1744,11 @@ public function testUpdateUrlRewritesOnImport() /** @var \Magento\Catalog\Model\Product $product */ $product = $this->objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); - + $listOfProductUrlKeys = [ + sprintf('%s.html', $product->getUrlKey()), + sprintf('men/tops/%s.html', $product->getUrlKey()), + sprintf('men/%s.html', $product->getUrlKey()) + ]; $repUrlRewriteCol = $this->objectManager->create( UrlRewriteCollection::class ); @@ -1754,18 +1758,15 @@ public function testUpdateUrlRewritesOnImport() ->addFieldToFilter('entity_id', ['eq'=> $product->getEntityId()]) ->addFieldToFilter('entity_type', ['eq'=> 'product']) ->load(); + $listOfUrlRewriteIds = $collUrlRewrite->getAllIds(); + $this->assertCount(3, $collUrlRewrite); - $this->assertCount(2, $collUrlRewrite); - - $this->assertEquals( - sprintf('%s.html', $product->getUrlKey()), - $collUrlRewrite->getFirstItem()->getRequestPath() - ); - - $this->assertContains( - sprintf('men/tops/%s.html', $product->getUrlKey()), - $collUrlRewrite->getLastItem()->getRequestPath() - ); + foreach ($listOfUrlRewriteIds as $key => $id) { + $this->assertEquals( + $listOfProductUrlKeys[$key], + $collUrlRewrite->getItemById($id)->getRequestPath() + ); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php index 4f8a279a59165..24ad6af1fea51 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/ResultTest.php @@ -8,6 +8,7 @@ /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php */ class ResultTest extends \Magento\TestFramework\TestCase\AbstractController { @@ -31,6 +32,9 @@ public function testIndexActionTranslation() $this->assertContains('Den gesamten Shop durchsuchen...', $responseBody); } + /** + * @magentoDbIsolation disabled + */ public function testIndexActionXSSQueryVerification() { $escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php index 4d2a90f8f44f9..d503c9678dfd6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProviderTest.php @@ -7,37 +7,71 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository as ProductRepository; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Api\Search\Document as SearchDocument; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Search\AdapterInterface as AdapterInterface; +use Magento\Framework\Search\Request\Builder as SearchRequestBuilder; +use Magento\Framework\Search\Request\Config as SearchRequestConfig; +use Magento\Search\Model\AdapterFactory as AdapterFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + class DataProviderTest extends \PHPUnit\Framework\TestCase { + /** + * @inheritdoc + */ + public static function setUpBeforeClass() + { + /* + * Due to insufficient search engine isolation for Elasticsearch, this class must explicitly perform + * a fulltext reindex prior to running its tests. + * + * This should be removed upon completing MC-19455. + */ + $indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + $fulltextIndexer = $indexRegistry->get(Fulltext::INDEXER_ID); + $fulltextIndexer->reindexAll(); + } + /** * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search.php * @magentoDbIsolation disabled */ public function testSearchProductByAttribute() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + + /** @var SearchRequestConfig $config */ + $config = $objectManager->create(SearchRequestConfig::class); - $config = $objectManager->create(\Magento\Framework\Search\Request\Config::class); - /** @var \Magento\Framework\Search\Request\Builder $requestBuilder */ + /** @var SearchRequestBuilder $requestBuilder */ $requestBuilder = $objectManager->create( - \Magento\Framework\Search\Request\Builder::class, + SearchRequestBuilder::class, ['config' => $config] ); + $requestBuilder->bind('search_term', 'VALUE1'); $requestBuilder->setRequestName('quick_search_container'); $queryRequest = $requestBuilder->create(); - /** @var \Magento\Framework\Search\Adapter\Mysql\Adapter $adapter */ - $adapter = $objectManager->create(\Magento\Framework\Search\Adapter\Mysql\Adapter::class); + + /** @var AdapterInterface $adapter */ + $adapterFactory = $objectManager->create(AdapterFactory::class); + $adapter = $adapterFactory->create(); $queryResponse = $adapter->query($queryRequest); $actualIds = []; + foreach ($queryResponse as $document) { - /** @var \Magento\Framework\Api\Search\Document $document */ + /** @var SearchDocument $document */ $actualIds[] = $document->getId(); } - /** @var \Magento\Catalog\Model\Product $product */ - $product = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class)->get('simple'); + /** @var Product $product */ + $product = $objectManager->create(ProductRepository::class)->get('simple'); $this->assertContains($product->getId(), $actualIds, 'Product not found by searchable attribute.'); } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 916af235edbd8..a5c18f0fcee6c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -16,38 +16,36 @@ /** * Class for testing fulltext index rebuild + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FullTest extends \PHPUnit\Framework\TestCase { - /** - * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full - */ - protected $actionFull; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->actionFull = Bootstrap::getObjectManager()->create( - \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class - ); - } - /** * Testing fulltext index rebuild * * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php + * @magentoConfigFixture default/catalog/search/engine mysql */ public function testGetIndexData() { + $engineProvider = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\ResourceModel\EngineProvider::class + ); + $dataProvider = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::class, + ['engineProvider' => $engineProvider] + ); + $actionFull = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class, + ['dataProvider' => $dataProvider] + ); /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); - $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); + $result = iterator_to_array($actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); $this->assertNotEmpty($result); $productsIds = array_keys($result); @@ -132,6 +130,9 @@ private function getExpectedIndexData() */ public function testRebuildStoreIndexConfigurable() { + $actionFull = Bootstrap::getObjectManager()->create( + \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class + ); $storeId = 1; $simpleProductId = $this->getIdBySku('simple_10'); @@ -141,8 +142,8 @@ public function testRebuildStoreIndexConfigurable() $simpleProductId, $configProductId ]; - $storeIndexDataSimple = $this->actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); - $storeIndexDataExpected = $this->actionFull->rebuildStoreIndex($storeId, $expected); + $storeIndexDataSimple = $actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); + $storeIndexDataExpected = $actionFull->rebuildStoreIndex($storeId, $expected); $this->assertEquals($storeIndexDataSimple, $storeIndexDataExpected); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php index 7c72d18b97118..b0ae104cae393 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php @@ -9,7 +9,6 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; use Magento\TestFramework\Helper\Bootstrap; /** @@ -76,10 +75,6 @@ protected function setUp() ); $this->indexer->load('catalogsearch_fulltext'); - $this->engine = Bootstrap::getObjectManager()->get( - \Magento\CatalogSearch\Model\ResourceModel\Engine::class - ); - $this->queryFactory = Bootstrap::getObjectManager()->get( \Magento\Search\Model\QueryFactory::class ); @@ -223,12 +218,8 @@ protected function search(string $text, $visibilityFilter = null): array $query->setQueryText($text); $query->saveIncrementalPopularity(); $products = []; - $collection = Bootstrap::getObjectManager()->create( - Collection::class, - [ - 'searchRequestName' => 'quick_search_container' - ] - ); + $searchLayer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Layer\Search::class); + $collection = $searchLayer->getProductCollection(); $collection->addSearchFilter($text); if (null !== $visibilityFilter) { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php index f0c8402c51879..b75a984178f24 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/DecimalTest.php @@ -48,6 +48,46 @@ protected function setUp() ->create(\Magento\CatalogSearch\Model\Layer\Filter\Decimal::class, ['layer' => $layer]); $this->_model->setAttributeModel($attribute); } + + /** + * Test the filter label is correct + */ + public function testApplyFilterLabel() + { + /** @var $objectManager \Magento\TestFramework\ObjectManager */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $request \Magento\TestFramework\Request */ + $request = $objectManager->get(\Magento\TestFramework\Request::class); + $request->setParam('weight', '10-20'); + $this->_model->apply($request); + + $filters = $this->_model->getLayer()->getState()->getFilters(); + $this->assertArrayHasKey(0, $filters); + $this->assertEquals( + '<span class="price">$10.00</span> - <span class="price">$19.99</span>', + (string)$filters[0]->getLabel() + ); + } + + /** + * Test the filter label is correct when there is empty To value + */ + public function testApplyFilterLabelWithEmptyToValue() + { + /** @var $objectManager \Magento\TestFramework\ObjectManager */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $request \Magento\TestFramework\Request */ + $request = $objectManager->get(\Magento\TestFramework\Request::class); + $request->setParam('weight', '10-'); + $this->_model->apply($request); + + $filters = $this->_model->getLayer()->getState()->getFilters(); + $this->assertArrayHasKey(0, $filters); + $this->assertEquals( + '<span class="price">$10.00</span> and above', + (string)$filters[0]->getLabel() + ); + } public function testApplyNothing() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php index 451553113af2c..a7944566eb8e0 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Layer/Filter/PriceTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Layer\Filter; use Magento\TestFramework\Helper\Bootstrap; @@ -35,10 +37,16 @@ protected function setUp() $category->load(4); $layer = $this->objectManager->get(\Magento\Catalog\Model\Layer\Category::class); $layer->setCurrentCategory($category); + /** @var $attribute \Magento\Catalog\Model\Entity\Attribute */ + $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Entity\Attribute::class + ); + $attribute->loadByCode('catalog_product', 'price'); $this->_model = $this->objectManager->create( \Magento\CatalogSearch\Model\Layer\Filter\Price::class, ['layer' => $layer] ); + $this->_model->setAttributeModel($attribute); } public function testApplyNothing() diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php index 5dcff3f92a9f9..87fda534be6d9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Advanced/CollectionTest.php @@ -18,8 +18,9 @@ class CollectionTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->advancedCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class); + $advanced = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\CatalogSearch\Model\Search\ItemCollectionProvider::class); + $this->advancedCollection = $advanced->getCollection(); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php index 93df194080b69..ae4fbc8d0d98e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php @@ -14,6 +14,9 @@ class CollectionTest extends \PHPUnit\Framework\TestCase /** * @dataProvider filtersDataProviderSearch * @magentoDataFixture Magento/Framework/Search/_files/products.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * @magentoConfigFixture default/catalog/search/engine mysql + * @magentoAppIsolation enabled */ public function testLoadWithFilterSearch($request, $filters, $expectedCount) { @@ -31,6 +34,42 @@ public function testLoadWithFilterSearch($request, $filters, $expectedCount) $this->assertCount($expectedCount, $items); } + /** + * @dataProvider filtersDataProviderQuickSearch + * @magentoDataFixture Magento/Framework/Search/_files/products.php + */ + public function testLoadWithFilterQuickSearch($filters, $expectedCount) + { + $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Search::class); + /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ + $fulltextCollection = $searchLayer->getProductCollection(); + foreach ($filters as $field => $value) { + $fulltextCollection->addFieldToFilter($field, $value); + } + $fulltextCollection->loadWithFilter(); + $items = $fulltextCollection->getItems(); + $this->assertCount($expectedCount, $items); + } + + /** + * @dataProvider filtersDataProviderCatalogView + * @magentoDataFixture Magento/Framework/Search/_files/products.php + */ + public function testLoadWithFilterCatalogView($filters, $expectedCount) + { + $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Category::class); + /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ + $fulltextCollection = $searchLayer->getProductCollection(); + foreach ($filters as $field => $value) { + $fulltextCollection->addFieldToFilter($field, $value); + } + $fulltextCollection->loadWithFilter(); + $items = $fulltextCollection->getItems(); + $this->assertCount($expectedCount, $items); + } + /** * @magentoDataFixture Magento/Framework/Search/_files/products_with_the_same_search_score.php */ @@ -42,11 +81,9 @@ public function testSearchResultsAreTheSameForSameRequests() $objManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); foreach (range(1, $howManySearchRequests) as $i) { + $searchLayer = $objManager->create(\Magento\Catalog\Model\Layer\Search::class); /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $fulltextCollection */ - $fulltextCollection = $objManager->create( - \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, - ['searchRequestName' => 'quick_search_container'] - ); + $fulltextCollection = $searchLayer->getProductCollection(); $fulltextCollection->addFieldToFilter('search_term', 'shorts'); $fulltextCollection->setOrder('relevance'); @@ -81,4 +118,22 @@ public function filtersDataProviderSearch() ['catalog_view_container', [], 0], ]; } + + public function filtersDataProviderQuickSearch() + { + return [ + [['search_term' => ' shorts'], 2], + [['search_term' => 'nonexistent'], 0], + ]; + } + + public function filtersDataProviderCatalogView() + { + return [ + [['category_ids' => 2], 5], + [['category_ids' => 100001], 0], + [['category_ids' => []], 5], + [[], 5], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php new file mode 100644 index 0000000000000..687b997eedde7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model; + +use Magento\Catalog\Model\CategoryFactory; +use Magento\Catalog\Model\CategoryRepository; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; + +/** + * Class for category url rewrites tests + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class CategoryUrlRewriteTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryFactory */ + private $categoryFactory; + + /** @var UrlRewriteCollectionFactory */ + private $urlRewriteCollectionFactory; + + /** @var CategoryRepository */ + private $categoryRepository; + + /** @var CategoryResource */ + private $categoryResource; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryFactory = $this->objectManager->get(CategoryFactory::class); + $this->urlRewriteCollectionFactory = $this->objectManager->get(UrlRewriteCollectionFactory::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); + $this->categoryResource = $this->objectManager->get(CategoryResource::class); + } + + /** + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @magentoDataFixture Magento/Catalog/_files/category_with_position.php + * @dataProvider categoryProvider + * @param array $data + * @return void + */ + public function testUrlRewriteOnCategorySave(array $data): void + { + $categoryModel = $this->categoryFactory->create(); + $categoryModel->isObjectNew(true); + $categoryModel->setData($data['data']); + $this->categoryResource->save($categoryModel); + $this->assertNotNull($categoryModel->getId(), 'The category was not created'); + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $urlRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, ['eq' => $categoryModel->getId()]) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE]); + + foreach ($urlRewriteCollection as $item) { + foreach ($data['expected_data'] as $field => $expectedItem) { + $this->assertEquals( + sprintf($expectedItem, $categoryModel->getId()), + $item[$field], + 'The expected data does not match actual value' + ); + } + } + } + + /** + * @return array + */ + public function categoryProvider(): array + { + return [ + 'without_url_key' => [ + [ + 'data' => [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'path' => '1/2', + 'is_active' => true, + ], + 'expected_data' => [ + 'request_path' => 'test-category.html', + 'target_path' => 'catalog/category/view/id/%s', + ], + ], + ], + 'subcategory_without_url_key' => [ + [ + 'data' => [ + 'name' => 'Test Sub Category', + 'attribute_set_id' => '3', + 'parent_id' => 444, + 'path' => '1/2/444', + 'is_active' => true, + ], + 'expected_data' => [ + 'request_path' => 'category-1/test-sub-category.html', + 'target_path' => 'catalog/category/view/id/%s', + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php new file mode 100644 index 0000000000000..aba813148512c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/ConfigurableTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Block\Cart\Item\Renderer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable as ConfigurableRenderer; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; + +/** + * Test \Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable block + * + * @magentoAppArea frontend + */ +class ConfigurableTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ConfigurableRenderer + */ + private $block; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class) + ->createBlock(ConfigurableRenderer::class); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php + */ + public function testGetProductPriceHtml() + { + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->getById(1); + + $layout = $this->objectManager->get(LayoutInterface::class); + $layout->createBlock( + \Magento\Framework\Pricing\Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true + ] + ] + ); + + $this->block->setItem( + $this->block->getCheckoutSession()->getQuote()->getAllVisibleItems()[0] + ); + $html = $this->block->getProductPriceHtml($configurableProduct); + $this->assertContains('<span class="price">$10.00</span>', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php new file mode 100644 index 0000000000000..4de079ba3b6ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute2 = $eavConfig->getAttribute('catalog_product', 'test_configurable_2'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute2->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute2 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute2->setData( + [ + 'attribute_code' => 'test_configurable_2', + '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 2'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute2); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute2->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php new file mode 100644 index 0000000000000..84f6ec58d3e4f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_2_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_2'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php new file mode 100644 index 0000000000000..1bf816425a9c9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Setup\CategorySetup; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/configurable_attribute.php'; +require __DIR__ . '/configurable_attribute_2.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute2->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable2($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute2->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute2->getId(), + 'code' => $attribute2->getAttributeCode(), + 'label' => $attribute2->getStoreLabel(), + 'position' => '1', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php new file mode 100644 index 0000000000000..d4fa2a97c4934 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_different_super_attribute_rollback.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +require __DIR__ . '/configurable_products_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +require __DIR__ . '/configurable_attribute_2_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php index 9a2f5c49ac298..70aa7c07ed536 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php @@ -121,7 +121,7 @@ $registry->register('isSecureArea', false); $product->setTypeId(Configurable::TYPE_CODE) - ->setId(11) + ->setId(111) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Product 12345') diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php index 8e25e5960a4b5..15111b27783d9 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRatesTest.php @@ -6,11 +6,30 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\Escaper; + /** * Fetch Rates Test */ class FetchRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var Escaper + */ + private $escaper; + + /** + * Initial setup + */ + protected function setUp() + { + $this->escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Escaper::class + ); + + parent::setUp(); + } + /** * Test fetch action without service * @@ -46,7 +65,11 @@ public function testFetchRatesActionWithNonexistentService(): void $this->dispatch('backend/admin/system_currency/fetchRates'); $this->assertSessionMessages( - $this->contains("The import model can't be initialized. Verify the model and try again."), + $this->contains( + $this->escaper->escapeHtml( + "The import model can't be initialized. Verify the model and try again." + ) + ), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php index fefd1a7b250c3..536aadd190c0e 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -13,6 +15,11 @@ class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendContr /** @var \Magento\Directory\Model\Currency $currencyRate */ protected $currencyRate; + /** + * @var Escaper + */ + private $escaper; + /** * Initial setup */ @@ -21,6 +28,10 @@ protected function setUp() $this->currencyRate = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Directory\Model\Currency::class ); + $this->escaper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Escaper::class + ); + parent::setUp(); } @@ -89,7 +100,9 @@ public function testSaveWithWarningAction() $this->assertSessionMessages( $this->contains( - (string)__('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) + $this->escaper->escapeHtml( + (string)__('Please correct the input data for "%1 => %2" rate.', $currencyCode, $currencyTo) + ) ), \Magento\Framework\Message\MessageInterface::TYPE_WARNING ); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 566dfbadedd29..7116953d682b3 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -769,9 +769,21 @@ public function testConfirmationEmailWithSpecialCharacters(): void $message = $this->transportBuilderMock->getSentMessage(); $rawMessage = $message->getRawMessage(); - $this->assertContains('To: John Smith <' . $email . '>', $rawMessage); + /** @var \Zend\Mime\Part $messageBodyPart */ + $messageBodyParts = $message->getBody()->getParts(); + $messageBodyPart = reset($messageBodyParts); + $messageEncoding = $messageBodyPart->getCharset(); + $name = 'John Smith'; + + if (strtoupper($messageEncoding) !== 'ASCII') { + $name = \Zend\Mail\Header\HeaderWrap::mimeEncodeValue($name, $messageEncoding); + } + + $nameEmail = sprintf('%s <%s>', $name, $email); + + $this->assertContains('To: ' . $nameEmail, $rawMessage); - $content = $message->getBody()->getParts()[0]->getRawContent(); + $content = $messageBodyPart->getRawContent(); $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); $this->setRequestInfo($confirmationUrl, 'confirm'); $this->clearCookieMessagesList(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php new file mode 100644 index 0000000000000..d8c56b7bf6f4f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; + +require __DIR__ . '/customer.php'; + +$objectManager = Bootstrap::getObjectManager(); +$repository = $objectManager->create(CustomerRepositoryInterface::class); +$customer = $repository->get('customer@example.com'); +$customer->setStoreId(2); +$repository->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php new file mode 100644 index 0000000000000..61cce9dbcc8d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_store_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +include __DIR__ . '/customer_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php index 215dd2a709418..3a39e62af0ccb 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php @@ -30,7 +30,7 @@ )->setLastname( 'Alston' )->setGender( - 2 + '2' ); $customer->isObjectNew(true); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php index 05d9c5d3acb1e..77ceae27e0774 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php @@ -78,7 +78,7 @@ public function testImportData() $expectAddedCustomers = 5; $source = new \Magento\ImportExport\Model\Import\Source\Csv( - __DIR__ . '/_files/customers_to_import.csv', + __DIR__ . '/_files/customers_with_gender_to_import.csv', $this->directoryWrite ); @@ -133,6 +133,11 @@ public function testImportData() $updatedCustomer->getCreatedAt(), 'Creation date must be changed' ); + $this->assertEquals( + $existingCustomer->getGender(), + $updatedCustomer->getGender(), + 'Gender must be changed' + ); } /** diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv new file mode 100644 index 0000000000000..96c14c67607aa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv @@ -0,0 +1,7 @@ +email,_website,_store,confirmation,created_at,created_in,default_billing,default_shipping,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,rp_token,rp_token_created_at,store_id,suffix,taxvat,website_id,password +AnthonyANealy@magento.com,base,admin,,5/6/2012 15:53,Admin,1,1,0,5/6/2010,Anthony,Female,1,Nealy,A.,6a9c9bfb2ba88a6ad2a64e7402df44a763e0c48cd21d7af9e7e796cd4677ee28:RF,,,,0,,,1, +LoriBBanks@magento.com,admin,admin,,5/6/2012 15:59,Admin,3,3,0,5/6/2010,Lori,Female,1,Banks,B.,7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz,,,,0,,,0, +CharlesTAlston@teleworm.us,base,admin,,5/6/2012 16:13,Admin,4,4,0,,Jhon,Female,1,Doe,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +customer@example.com,base,admin,,5/6/2012 16:15,Admin,4,4,0,,Firstname,Female,1,Lastname,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +julie.worrell@example.com,base,admin,,5/6/2012 16:19,Admin,4,4,0,,Julie,Female,1,Worrell,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +david.lamar@example.com,base,admin,,5/6/2012 16:25,Admin,4,4,0,,David,,1,Lamar,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, diff --git a/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php b/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php index ea155894d7299..a9591f1968ef7 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/DeployTest.php @@ -71,6 +71,7 @@ class DeployTest extends \PHPUnit\Framework\TestCase private $options = [ Options::DRY_RUN => false, Options::NO_JAVASCRIPT => false, + Options::NO_JS_BUNDLE => false, Options::NO_CSS => false, Options::NO_LESS => false, Options::NO_IMAGES => false, @@ -100,9 +101,10 @@ protected function setUp() $this->rootDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); $logger = $objectManager->get(\Psr\Log\LoggerInterface::class); - $this->deployService = $objectManager->create(DeployStaticContent::class, [ - 'logger' => $logger - ]); + $this->deployService = $objectManager->create( + DeployStaticContent::class, + ['logger' => $logger] + ); $this->bundleConfig = $objectManager->create(BundleConfig::class); $this->config = $objectManager->create(View::class); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php new file mode 100644 index 0000000000000..7f1701d4ca6ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Downloadable\Api\Data\LinkInterface; +use Magento\Downloadable\Api\LinkRepositoryInterface; +use Magento\Downloadable\Helper\Download; +use Magento\Downloadable\Model\Link; +use Magento\Downloadable\Model\Product\Type; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); +/** @var LinkRepositoryInterface $linkRepository */ +$linkRepository = Bootstrap::getObjectManager() + ->create(LinkRepositoryInterface::class); +/** @var ProductInterface $product */ +$product = Bootstrap::getObjectManager() + ->create(ProductInterface::class); +/** @var LinkInterface $downloadableProductLink */ +$downloadableProductLink = Bootstrap::getObjectManager() + ->create(LinkInterface::class); + +$downloadableProductLink +// ->setId(null) + ->setLinkType(Download::LINK_TYPE_URL) + ->setTitle('Downloadable Product Link') + ->setIsShareable(Link::LINK_SHAREABLE_CONFIG) + ->setLinkUrl('http://example.com/downloadable.txt') + ->setNumberOfDownloads(100) + ->setSortOrder(1) + ->setPrice(0); + +$downloadableProductLinks[] = $downloadableProductLink; + +$product + ->setId(1) + ->setTypeId(Type::TYPE_DOWNLOADABLE) + ->setExtensionAttributes( + $product->getExtensionAttributes() + ->setDownloadableProductLinks($downloadableProductLinks) + ) + ->setSku('downloadable-product') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Downloadable Product Limited') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setLinksPurchasedSeparately(true) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php new file mode 100644 index 0000000000000..d88f6f1803434 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/product_downloadable_with_download_limit_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/product_downloadable_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php index fcd8226aec50c..787e554a947a1 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php @@ -13,7 +13,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Indexer\Category\Product as CategoryIndexer; use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; -use Magento\Elasticsearch\Model\Config; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; @@ -35,13 +34,6 @@ protected function setUp() { parent::setUp(); - $config = $this->getMockBuilder(Config::class) - ->disableOriginalConstructor() - ->getMock(); - $config->method('isElasticsearchEnabled') - ->willReturn(true); - $this->_objectManager->addSharedInstance($config, Config::class); - $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, true); $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, true); } @@ -51,7 +43,6 @@ protected function setUp() */ protected function tearDown() { - $this->_objectManager->removeSharedInstance(Config::class); $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, $this->indexerSchedule[FulltextIndexer::INDEXER_ID]); $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, $this->indexerSchedule[CategoryIndexer::INDEXER_ID]); @@ -161,9 +152,12 @@ private function getProductIdList(array $skuList): array $items = $repository->getList($searchCriteria) ->getItems(); - $idList = array_map(function (ProductInterface $item) { - return $item->getId(); - }, $items); + $idList = array_map( + function (ProductInterface $item) { + return $item->getId(); + }, + $items + ); return $idList; } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php new file mode 100644 index 0000000000000..2b531eac4e423 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch6/SearchAdapter/ConnectionManagerTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch6\SearchAdapter; + +use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\ConnectionManager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for \Magento\Elasticsearch\SearchAdapter\ConnectionManager class. + */ +class ConnectionManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Elasticsearch\SearchAdapter\ConnectionManager + */ + private $connectionManager; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + + $this->connectionManager = $this->objectManager->create(ConnectionManager::class); + } + + /** + * Test if 'elasticsearch5' search engine returned by connection manager. + * + * @magentoAppIsolation enabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch5 + */ + public function testCorrectElasticsearchClientEs5() + { + $connection = $this->connectionManager->getConnection(); + $this->assertInstanceOf( + \Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch::class, + $connection + ); + } + + /** + * Test if 'elasticsearch6' search engine returned by connection manager. + * + * @magentoAppIsolation enabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + */ + public function testCorrectElasticsearchClientEs6() + { + $connection = $this->connectionManager->getConnection(); + $this->assertInstanceOf( + \Magento\Elasticsearch6\Model\Client\Elasticsearch::class, + $connection + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php index bbbcc3133d4ba..6d5f760d7894d 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php @@ -10,9 +10,9 @@ $template->setOptions(['area' => 'test area', 'store' => 1]); $template->setData( [ - 'template_text' => - file_get_contents(__DIR__ . '/template_fixture.html'), - 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE + 'template_text' => file_get_contents(__DIR__ . '/template_fixture.html'), + 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE, + 'template_type' => \Magento\Email\Model\Template::TYPE_TEXT ] ); $template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php new file mode 100644 index 0000000000000..3324da008a84a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Cache/Frontend/PoolTest.php @@ -0,0 +1,55 @@ +<?php declare(strict_types=1); + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\App\Cache\Frontend; + +use Magento\Framework\ObjectManager\ConfigInterface as ObjectManagerConfig; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * This superfluous comment can be removed as soon as the sniffs have been updated to match the coding guide lines. + */ +class PoolTest extends TestCase +{ + public function testPageCacheNotSameAsDefaultCacheDirectory(): void + { + /** @var ObjectManagerConfig $diConfig */ + $diConfig = ObjectManager::getInstance()->get(ObjectManagerConfig::class); + $argumentConfig = $diConfig->getArguments(\Magento\Framework\App\Cache\Frontend\Pool::class); + + $pageCacheDir = $argumentConfig['frontendSettings']['page_cache']['backend_options']['cache_dir'] ?? null; + $defaultCacheDir = $argumentConfig['frontendSettings']['default']['backend_options']['cache_dir'] ?? null; + + $noPageCacheMessage = "No default page_cache directory set in di.xml: \n" . var_export($argumentConfig, true); + $this->assertNotEmpty($pageCacheDir, $noPageCacheMessage); + + $sameCacheDirMessage = 'The page_cache and default cache storages share the same cache directory'; + $this->assertNotSame($pageCacheDir, $defaultCacheDir, $sameCacheDirMessage); + } + + /** + * @covers \Magento\Framework\App\Cache\Frontend\Pool::_getCacheSettings + * @depends testPageCacheNotSameAsDefaultCacheDirectory + */ + public function testCleaningDefaultCachePreservesPageCache() + { + $testData = 'test data'; + $testKey = 'test-key'; + + /** @var \Magento\Framework\App\Cache\Frontend\Pool $cacheFrontendPool */ + $cacheFrontendPool = ObjectManager::getInstance()->get(\Magento\Framework\App\Cache\Frontend\Pool::class); + + $pageCache = $cacheFrontendPool->get('page_cache'); + $pageCache->save($testData, $testKey); + + $defaultCache = $cacheFrontendPool->get('default'); + $defaultCache->clean(); + + $this->assertSame($testData, $pageCache->load($testKey)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json index fe1d382b361fa..404db202c6e72 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromClone/composer.json @@ -17,7 +17,6 @@ "ext-intl": "*", "ext-mcrypt": "*", "ext-simplexml": "*", - "ext-spl": "*", "lib-libxml": "*", "composer/composer": "1.0.0-alpha9", "magento/magento-composer-installer": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php new file mode 100644 index 0000000000000..03bdc9a365526 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Email\Model\BackendTemplate; +use Magento\Email\Model\Template; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class EmailMessageTest + */ +class TransportBuilderTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $di; + + /** + * @var TransportBuilder + */ + protected $builder; + + /** + * @var Template + */ + protected $template; + + protected function setUp() + { + $this->di = Bootstrap::getObjectManager(); + $this->builder = $this->di->get(TransportBuilder::class); + $this->template = $this->di->get(Template::class); + } + + /** + * @magentoDataFixture Magento/Email/Model/_files/email_template.php + * @magentoDbIsolation enabled + * + * @param string|array $email + * @dataProvider emailDataProvider + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddToEmail($email) + { + $templateId = $this->template->load('email_exception_fixture', 'template_code')->getId(); + + $this->builder->setTemplateModel(BackendTemplate::class); + + $vars = ['reason' => 'Reason', 'customer' => 'Customer']; + $options = ['area' => 'frontend', 'store' => 1]; + $this->builder->setTemplateIdentifier($templateId)->setTemplateVars($vars)->setTemplateOptions($options); + + $this->builder->addTo($email); + + /** @var EmailMessage $emailMessage */ + $emailMessage = $this->builder->getTransport(); + + $addresses = $emailMessage->getMessage()->getTo(); + + $emails = []; + /** @var Address $toAddress */ + foreach ($addresses as $address) { + $emails[] = $address->getEmail(); + } + + if (is_string($email)) { + $this->assertCount(1, $emails); + $this->assertEquals($email, $emails[0]); + } else { + $this->assertEquals($email, $emails); + } + } + + /** + * @return array + */ + public function emailDataProvider(): array + { + return [ + [ + 'billy.everything@someserver.com', + ], + [ + [ + 'billy.everything@someserver.com', + 'john.doe@someserver.com', + ] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/styles.magento.min.css b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/styles.magento.min.css index 7bb283e0b3514..bb901988c5b8a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/styles.magento.min.css +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/expected/styles.magento.min.css @@ -1 +1 @@ -table>caption{margin-bottom:5px}table thead{background:#676056;color:#f7f3eb}table thead .headings{background:#807a6e}table thead a{color:#f7f3eb;display:block}table thead a label{color:#f7f3eb;cursor:pointer;display:block}table thead a:hover,table thead a:focus{color:#dac7a2;text-decoration:none}table tfoot{background:#f2ebde;color:#676056}table tfoot tr th,table tfoot tr td{text-align:left}table th{background:0 0;border:solid #cac3b4;border-width:0 1px;font-size:14px;padding:6px 10px;text-align:center}table td{border:solid #cac3b4;border-width:0 1px;padding:6px 10px 7px;vertical-align:top}table tbody tr td{background:#fff;color:#676056;padding-top:12px}table tbody tr td:first-child{border-left:0}table tbody tr td:first-child input[type=checkbox]{margin:0}table tbody tr td:last-child{border-right:0}table tbody tr:last-child th,table tbody tr:last-child td{border-bottom-width:1px}table tbody tr:nth-child(odd) td,table tbody tr:nth-child(odd) th{background-color:#f7f3eb}table tbody.even tr td{background:#fff}table tbody.odd tr td{background:#f7f3eb}table .dropdown-menu li{padding:7px 15px;line-height:14px;cursor:pointer}table .col-draggable .draggable-handle{float:left;position:relative;top:0}.not-sort{padding-right:10px}.sort-arrow-asc,.sort-arrow-desc{padding-right:10px;position:relative}.sort-arrow-asc:after,.sort-arrow-desc:after{right:-11px;top:-1px;position:absolute;width:23px}.sort-arrow-asc:hover:after,.sort-arrow-desc:hover:after{color:#dac7a2}.sort-arrow-asc{display:inline-block;text-decoration:none}.sort-arrow-asc:after{font-family:'icons-blank-theme';content:'\e626';font-size:13px;line-height:inherit;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.sort-arrow-asc:hover:after{color:#dac7a2}.sort-arrow-desc{display:inline-block;text-decoration:none}.sort-arrow-desc:after{font-family:'icons-blank-theme';content:'\e623';font-size:13px;line-height:inherit;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.sort-arrow-desc:hover:after{color:#dac7a2}.grid-actions .input-text,.pager .input-text,.massaction .input-text,.filter .input-text,.grid-actions select,.pager select,.massaction select,.filter select,.grid-actions .select,.pager .select,.massaction .select,.filter .select{border-color:#989287;box-shadow:none;border-radius:1px;height:28px;margin:0 10px 0 0}.filter th{border:0 solid #676056;padding:6px 3px;vertical-align:top}.filter .ui-datepicker-trigger{cursor:pointer;margin-top:2px}.filter .input-text{padding:0 5px}.filter .range-line:not(:last-child){margin-bottom:5px}.filter .date{padding-right:28px;position:relative;display:inline-block;text-decoration:none}.filter .date .hasDatepicker{vertical-align:top;width:99%}.filter .date img{cursor:pointer;height:25px;width:25px;right:0;position:absolute;vertical-align:middle;z-index:2;opacity:0}.filter .date:before{font-family:'icons-blank-theme';content:'\e612';font-size:42px;line-height:30px;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filter .date:hover:before{color:#dac7a2}.filter .date:before{height:29px;margin-left:5px;position:absolute;right:-3px;top:-3px;width:35px}.filter select{border-color:#cac3b4;margin:0;padding:0;width:99%}.filter input.input-text{border-color:#cac3b4;margin:0;width:99%}.filter input.input-text::-webkit-input-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text::-moz-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text:-moz-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text:-ms-input-placeholder{color:#989287 !important;text-transform:lowercase}.grid{background:#fff;color:#676056;font-size:13px;font-weight:400;padding:15px}.grid table{width:100%}.grid tbody tr.selected th,.grid tbody tr.selected td,.grid tbody tr:hover th,.grid tbody tr:hover td,.grid tbody tr:nth-child(odd):hover th,.grid tbody tr:nth-child(odd):hover td{background-color:#f2ebde;cursor:pointer}.grid tbody tr.selected th.empty-text,.grid tbody tr.selected td.empty-text,.grid tbody tr:hover th.empty-text,.grid tbody tr:hover td.empty-text,.grid tbody tr:nth-child(odd):hover th.empty-text,.grid tbody tr:nth-child(odd):hover td.empty-text{background-color:#f7f3eb;cursor:default}.grid .empty-text{font:400 20px/1.2 'Open Sans',sans-serif;text-align:center;white-space:nowrap}.grid .col-sku{max-width:100px;width:100px}.grid .col-select,.grid .col-massaction{text-align:center}.grid .editable .input-text{width:65px}.grid .col-actions .action-select{background:#fff;border-color:#989287;height:28px;margin:0;padding:4px 4px 5px;width:80px}.grid .col-position.editable{white-space:nowrap}.grid .col-position.editable .input-text{margin:-7px 5px 0;width:70%}.eq-ie9 .hor-scroll{display:inline-block;min-height:0;overflow-y:hidden;overflow-x:auto;width:100%}.data-table{border-collapse:separate;width:100%}.data-table thead,.data-table tfoot,.data-table th,.accordion .config .data-table thead th,.accordion .config .data-table tfoot td,.accordion .config .accordion .config .data-table tfoot td th{background:#fff;color:#676056;font-size:13px;font-weight:600}.data-table th{text-align:left}.data-table thead th,.accordion .config .data-table thead th th,.accordion .config .data-table tfoot td th,.accordion .config .accordion .config .data-table tfoot td th th{border:solid #c9c2b8;border-width:0 0 1px;padding:7px}.data-table td,.data-table tbody tr th,.data-table tbody tr td,.accordion .config .data-table td{background:#fff;border-width:0;padding:5px 7px;vertical-align:middle}.data-table tbody tr:nth-child(odd) th,.data-table tbody tr:nth-child(odd) td,.accordion .config .data-table tbody tr:nth-child(odd) td{background:#fbfaf6}.data-table tbody.odd tr th,.data-table tbody.odd tr td{background:#fbfaf6}.data-table tbody.even tr th,.data-table tbody.even tr td{background:#fff}.data-table tfoot tr:last-child th,.data-table tfoot tr:last-child td,.data-table .accordion .config .data-table tfoot tr:last-child td{border:0}.data-table.order-tables tbody td{vertical-align:top}.data-table.order-tables tbody:hover tr th,.data-table.order-tables tbody:hover tr td{background:#f7f3eb}.data-table.order-tables tfoot td{background:#f2ebde;color:#676056;font-size:13px;font-weight:600}.data-table input[type=text]{width:98%;padding-left:1%;padding-right:1%}.data-table select{margin:0;box-sizing:border-box}.data-table .col-actions .actions-split{margin-top:4px}.data-table .col-actions .actions-split [class^=action-]{background:0 0;border:1px solid #c8c3b5;padding:3px 5px;color:#bbb3a6;font-size:12px}.data-table .col-actions .actions-split [class^=action-]:first-child{border-right:0}.data-table .col-actions .actions-split .dropdown-menu{margin-top:-1px}.data-table .col-actions .actions-split .dropdown-menu a{display:block;color:#333;text-decoration:none}.data-table .col-actions .actions-split.active .action-toggle{position:relative;border-bottom-right-radius:0;box-shadow:none;background:#fff}.data-table .col-actions .actions-split.active .action-toggle:after{position:absolute;top:100%;left:0;right:0;height:2px;margin-top:-1px;background:#fff;content:'';z-index:2}.data-table .col-actions .actions-split.active .action-toggle .dropdown-menu{border-top-right-radius:0}.data-table .col-default{white-space:nowrap;text-align:center;vertical-align:middle}.data-table .col-delete{text-align:center;width:32px}.data-table .col-file{white-space:nowrap}.data-table .col-file input,.data-table .col-file .input-text{margin:0 5px;width:40%}.data-table .col-file input:first-child,.data-table .col-file .input-text:first-child{margin-left:0}.data-table .col-actions-add{padding:10px 0}.grid-actions{background:#fff;font-size:13px;line-height:28px;padding:10px 15px;position:relative}.grid-actions+.grid{padding-top:5px}.grid-actions .export,.grid-actions .filter-actions{float:right;margin-left:10px;vertical-align:top}.grid-actions .import{display:block;vertical-align:top}.grid-actions .action-reset{background:0 0;border:0;display:inline;line-height:1.42857143;margin:0;padding:0;color:#1979c3;text-decoration:none;margin:6px 10px 0 0;vertical-align:top}.grid-actions .action-reset:visited{color:purple;text-decoration:none}.grid-actions .action-reset:hover{color:#006bb4;text-decoration:underline}.grid-actions .action-reset:active{color:#ff5501;text-decoration:underline}.grid-actions .action-reset:hover{color:#006bb4}.grid-actions .action-reset:hover,.grid-actions .action-reset:active,.grid-actions .action-reset:focus{background:0 0;border:0}.grid-actions .action-reset.disabled,.grid-actions .action-reset[disabled],fieldset[disabled] .grid-actions .action-reset{color:#1979c3;text-decoration:underline;cursor:default;pointer-events:none;opacity:.5}.grid-actions .import .label,.grid-actions .export .label,.massaction>.entry-edit .label{margin:0 14px 0 0;vertical-align:inherit}.grid-actions .import .action-,.grid-actions .export .action-,.grid-actions .filter-actions .action-,.massaction>.entry-edit .action-{vertical-align:inherit}.grid-actions .filter .date{float:left;margin:0 15px 0 0;position:relative}.grid-actions .filter .date:before{color:#676056;top:1px}.grid-actions .filter .date:hover:before{color:#31302b}.grid-actions .filter .label{margin:0}.grid-actions .filter .hasDatepicker{margin:0 5px;width:80px}.grid-actions .filter .show-by .select{margin-left:5px;padding:4px 4px 5px;vertical-align:top;width:auto}.grid-actions .filter.required:after{content:''}.grid-actions img{vertical-align:middle;height:22px;width:22px}.grid-actions .validation-advice{background:#f9d4d4;border:1px solid #e22626;border-radius:3px;color:#e22626;margin:5px 0 0;padding:3px 7px;position:absolute;white-space:nowrap;z-index:5}.grid-actions .validation-advice:before{width:0;height:0;border:5px solid transparent;border-bottom-color:#e22626;content:'';left:50%;margin-left:-5px;position:absolute;top:-11px}.grid-actions input[type=text].validation-failed{border-color:#e22626;box-shadow:0 0 8px rgba(226,38,38,.6)}.grid-actions .link-feed{white-space:nowrap}.pager{font-size:13px}.grid .pager{margin:15px 0 0;position:relative;text-align:center}.pager .pages-total-found{margin-right:25px}.pager .view-pages .select{margin:0 5px}.pager .link-feed{font-size:12px;margin:7px 15px 0 0;position:absolute;right:0;top:0}.pager .action-previous,.pager .action-next{background:0 0;border:0;display:inline;line-height:1.42857143;margin:0;padding:0;color:#1979c3;text-decoration:none;line-height:.6;overflow:hidden;width:20px}.pager .action-previous:visited,.pager .action-next:visited{color:purple;text-decoration:none}.pager .action-previous:hover,.pager .action-next:hover{color:#006bb4;text-decoration:underline}.pager .action-previous:active,.pager .action-next:active{color:#ff5501;text-decoration:underline}.pager .action-previous:hover,.pager .action-next:hover{color:#006bb4}.pager .action-previous:hover,.pager .action-next:hover,.pager .action-previous:active,.pager .action-next:active,.pager .action-previous:focus,.pager .action-next:focus{background:0 0;border:0}.pager .action-previous.disabled,.pager .action-next.disabled,.pager .action-previous[disabled],.pager .action-next[disabled],fieldset[disabled] .pager .action-previous,fieldset[disabled] .pager .action-next{color:#1979c3;text-decoration:underline;cursor:default;pointer-events:none;opacity:.5}.pager .action-previous:before,.pager .action-next:before{margin-left:-10px}.pager .action-previous.disabled,.pager .action-next.disabled{opacity:.3}.pager .action-previous{display:inline-block;text-decoration:none}.pager .action-previous>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.pager .action-previous>span.focusable:active,.pager .action-previous>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-previous>span.focusable:active,.pager .action-previous>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-previous:before{font-family:'icons-blank-theme';content:'\e617';font-size:40px;line-height:inherit;color:#026294;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.pager .action-previous:hover:before{color:#007dbd}.pager .action-next{display:inline-block;text-decoration:none}.pager .action-next>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.pager .action-next>span.focusable:active,.pager .action-next>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-next>span.focusable:active,.pager .action-next>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-next:before{font-family:'icons-blank-theme';content:'\e608';font-size:40px;line-height:inherit;color:#026294;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.pager .action-next:hover:before{color:#007dbd}.pager .input-text{height:25px;line-height:16px;margin-right:5px;text-align:center;width:25px;vertical-align:top}.pager .pages-total{line-height:25px;vertical-align:top}.massaction{background:#fff;border-top:1px solid #f2ebde;font-size:13px;line-height:28px;padding:15px 15px 0}.massaction>.entry-edit{float:right}.massaction>.entry-edit .field-row{display:inline-block;vertical-align:top}.massaction>.entry-edit .validation-advice{display:none !important}.massaction>.entry-edit .form-inline{display:inline-block}.massaction>.entry-edit .label{padding:0;width:auto}.massaction>.entry-edit .action-{vertical-align:top}.massaction .select.validation-failed{border:1px dashed #e22626;background:#f9d4d4}.grid-severity-critical,.grid-severity-major,.grid-severity-notice,.grid-severity-minor{background:#feeee1;border:1px solid #ed4f2e;color:#ed4f2e;display:block;padding:0 3px;font-weight:700;line-height:17px;text-transform:uppercase;text-align:center}.grid-severity-critical,.grid-severity-major{border-color:#e22626;background:#f9d4d4;color:#e22626}.grid-severity-notice{border-color:#5b8116;background:#d0e5a9;color:#185b00}.grid tbody td input[type=text],.data-table tbody td input[type=text],.grid tbody th input[type=text],.data-table tbody th input[type=text],.grid tbody td .input-text,.data-table tbody td .input-text,.grid tbody th .input-text,.data-table tbody th .input-text,.grid tbody td select,.data-table tbody td select,.grid tbody th select,.data-table tbody th select,.grid tbody td .select,.data-table tbody td .select,.grid tbody th .select,.data-table tbody th .select{width:99%}.ui-tabs-panel .grid .col-sku{max-width:150px;width:150px}.col-indexer_status,.col-indexer_mode{width:160px}.fieldset-wrapper .grid-actions+.grid{padding-top:15px}.fieldset-wrapper .grid-actions{padding:10px 0 0}.fieldset-wrapper .grid{padding:0}.fieldset-wrapper .massaction{padding:0;border-top:none;margin-bottom:15px}.accordion .grid{padding:0}.ui-dialog-content .grid-actions,.ui-dialog-content .grid{padding-left:0;padding-right:0}.qty-table td{border:0;padding:0 5px 3px}.sales-order-create-index .sales-order-create-index .grid table .action-configure{float:right}.sales-order-create-index .data-table .border td{padding-bottom:15px}.sales-order-create-index .actions.update{margin:10px 0}.adminhtml-order-shipment-new .grid .col-product{max-width:770px;width:770px}.customer-index-index .grid .col-name{max-width:90px;width:90px}.customer-index-index .grid .col-billing_region{width:70px}.adminhtml-cms-hierarchy-index .col-title,.adminhtml-cms-hierarchy-index .col-identifier{max-width:410px;width:410px}.adminhtml-widget-instance-edit .grid-chooser .control{margin-top:-19px;width:80%}.eq-ie9 .adminhtml-widget-instance-edit .grid-chooser .control{margin-top:-18px}.adminhtml-widget-instance-edit .grid-chooser .control .grid-actions{padding:0 0 15px}.adminhtml-widget-instance-edit .grid-chooser .control .grid{padding:0}.adminhtml-widget-instance-edit .grid-chooser .control .addon input:last-child,.adminhtml-widget-instance-edit .grid-chooser .control .addon select:last-child{border-radius:0}.reports-report-product-sold .grid .col-name{max-width:720px;width:720px}.adminhtml-system-store-index .grid td{max-width:310px}.adminhtml-system-currency-index .grid{padding-top:0}.adminhtml-system-currency-index .col-currency-edit-rate{min-width:40px}.adminhtml-system-currency-index .col-base-currency{font-weight:700}.adminhtml-system-currency-index .old-rate{display:block;margin-top:3px;text-align:center}.adminhtml-system-currency-index .hor-scroll{overflow-x:auto;min-width:970px}.adminhtml-system-currencysymbol-index .col-currency{width:35%}.adminhtml-system-currencysymbol-index .grid .input-text{margin:0 10px 0 0;width:50%}.catalog-product-set-index .col-set_name{max-width:930px;width:930px}.adminhtml-export-index .grid td{vertical-align:middle}.adminhtml-export-index .grid .input-text-range{margin:0 10px 0 5px;width:37%}.adminhtml-export-index .grid .input-text-range-date{margin:0 5px;width:32%}.adminhtml-export-index .ui-datepicker-trigger{display:inline-block;margin:-3px 10px 0 0;vertical-align:middle}.adminhtml-notification-index .grid .col-select,.adminhtml-cache-index .grid .col-select,.adminhtml-process-list .grid .col-select,.indexer-indexer-list .grid .col-select{width:10px}@font-face{font-family:'icons-blank-theme';src:url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff2') format('woff2'),url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff') format('woff');font-weight:400;font-style:normal}@font-face{font-family:'icons-blank-theme';src:url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff2') format('woff2'),url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff') format('woff');font-weight:400;font-style:normal}.navigation{background-color:#676056;position:relative;z-index:5}.navigation .level-0.reverse>.submenu{right:1px}.navigation>ul{position:relative;text-align:right}.navigation .level-0>.submenu{display:none;position:absolute;top:100%;padding:19px 13px}.navigation .level-0>.submenu a{display:block;color:#676056;font-size:13px;font-weight:400;line-height:1.385;padding:3px 12px 3px;text-decoration:none}.navigation .level-0>.submenu a:focus,.navigation .level-0>.submenu a:hover{text-decoration:underline}.navigation .level-0>.submenu a:hover{color:#fff;background:#989287;text-decoration:none}.navigation .level-0>.submenu li{margin-bottom:1px}.navigation .level-0>.submenu a[href="#"]{cursor:default;display:block;color:#676056;font-size:14px;font-weight:700;line-height:1;margin:7px 0 6px;padding:0 12px}.navigation .level-0>.submenu a[href="#"]:focus,.navigation .level-0>.submenu a[href="#"]:hover{color:#676056;font-size:14px;font-weight:700;background:0 0;text-decoration:none}.navigation .level-0{display:inline-block;float:left;text-align:left;transition:display .15s ease-out}.navigation .level-0>a{background:0 0;display:block;padding:12px 13px 0;color:#f2ebde;font-size:13px;font-weight:600;text-transform:uppercase;text-decoration:none;transition:background .15s ease-out}.navigation .level-0>a:after{content:"";display:block;margin-top:10px;height:3px;font-size:0}.navigation .level-0.active>a{font-weight:700}.navigation .level-0.active>a:after{background:#ef672f}.navigation .level-0.hover.recent>a{background:#fff;color:#676056;font-size:13px;font-weight:600}.navigation .level-0.hover.recent>a:after{background:0 0}.navigation .level-0.hover.recent.active>a{font-weight:700}.navigation .level-0>.submenu{opacity:0;visibility:hidden}.navigation .level-0.recent.hover>.submenu{opacity:1;visibility:visible}.no-js .navigation .level-0:hover>.submenu,.no-js .navigation .level-0.hover>.submenu,.no-js .navigation .level-0>a:focus+.submenu{display:block}.navigation .level-0>.submenu{background:#fff;box-shadow:0 3px 3px rgba(50,50,50,.15)}.navigation .level-0>.submenu li{max-width:200px}.navigation .level-0>.submenu>ul{white-space:nowrap}.navigation .level-0>.submenu .column{display:inline-block;margin-left:40px;vertical-align:top}.navigation .level-0>.submenu .column:first-child{margin-left:0}.navigation .level-0 .submenu .level-1{white-space:normal}.navigation .level-0.parent .submenu .level-1.parent{margin:17px 0 25px}.navigation .level-0.parent .level-1.parent:first-child{margin-top:0}.navigation .level-2 .submenu{margin-left:7px}.navigation .level-0>.submenu .level-2>a[href="#"]{font-size:13px;margin-top:10px;margin-left:7px}.navigation .level-2>.submenu a{font-size:12px;line-height:1.231}.navigation .level-0>.submenu .level-3>a[href="#"],.navigation .level-3 .submenu{margin-left:15px}.navigation .level-0.item-system,.navigation .level-0.item-stores{float:none}.navigation .level-0.item-system>.submenu,.navigation .level-0.item-stores>.submenu{left:auto;right:1px}.adminhtml-dashboard-index .col-1-layout{max-width:1300px;border:none;border-radius:0;padding:0;background:#f7f3eb}.dashboard-inner{padding-top:35px}.dashboard-inner:before,.dashboard-inner:after{content:"";display:table}.dashboard-inner:after{clear:both}.dashboard-inner:before,.dashboard-inner:after{content:"";display:table}.dashboard-inner:after{clear:both}.dashboard-secondary{float:left;width:32%;margin:0 1.5%}.dashboard-main{float:right;width:65%}.dashboard-diagram-chart{max-width:100%;height:auto}.dashboard-diagram-nodata,.dashboard-diagram-switcher{padding:20px 0}.dashboard-diagram-image{background:#fff url(../mui/images/ajax-loader-small.gif) no-repeat 50% 50%}.dashboard-container .ui-tabs-panel{background-color:#fff;min-height:40px;padding:15px}.dashboard-store-stats{margin-top:35px}.dashboard-store-stats .ui-tabs-panel{background:#fff url(../mui/images/ajax-loader-small.gif) no-repeat 50% 50%}.dashboard-item{margin-bottom:30px}.dashboard-item-header{margin-left:5px}.dashboard-item.dashboard-item-primary{margin-bottom:35px}.dashboard-item.dashboard-item-primary .title{font-size:22px;margin-bottom:5px}.dashboard-item.dashboard-item-primary .dashboard-sales-value{display:block;text-align:right;font-weight:600;font-size:30px;margin-right:12px;padding-bottom:5px}.dashboard-item.dashboard-item-primary:first-child{color:#ef672f}.dashboard-item.dashboard-item-primary:first-child .title{color:#ef672f}.dashboard-totals{background:#fff;padding:50px 15px 25px}.dashboard-totals-list{margin:0;padding:0;list-style:none none}.dashboard-totals-list:before,.dashboard-totals-list:after{content:"";display:table}.dashboard-totals-list:after{clear:both}.dashboard-totals-list:before,.dashboard-totals-list:after{content:"";display:table}.dashboard-totals-list:after{clear:both}.dashboard-totals-item{float:left;width:18%;margin-left:7%;padding-top:15px;border-top:2px solid #cac3b4}.dashboard-totals-item:first-child{margin-left:0}.dashboard-totals-label{display:block;font-size:16px;font-weight:600;padding-bottom:2px}.dashboard-totals-value{color:#ef672f;font-size:20px}.dashboard-data{width:100%}.dashboard-data thead{background:0 0}.dashboard-data thead tr{background:0 0}.dashboard-data th,.dashboard-data td{border:none;padding:10px 12px;text-align:right}.dashboard-data th:first-child,.dashboard-data td:first-child{text-align:left}.dashboard-data th{color:#676056;font-weight:600}.dashboard-data td{background-color:transparent}.dashboard-data tbody tr:hover td{background-color:transparent}.dashboard-data tbody tr:nth-child(odd) td,.dashboard-data tbody tr:nth-child(odd):hover td,.dashboard-data tbody tr:nth-child(odd) th,.dashboard-data tbody tr:nth-child(odd):hover th{background-color:#e1dbcf}.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd) td,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd):hover td,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd) th,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd):hover th{background-color:#f7f3eb}.dashboard-data td.empty-text{text-align:center}.ui-tabs-panel .dashboard-data{background-color:#fff}.mage-dropdown-dialog.ui-dialog .ui-dialog-content{overflow:visible}.mage-dropdown-dialog.ui-dialog .ui-dialog-buttonpane{padding:0}.message-system-inner{background:#f7f3eb;border:1px solid #c0bbaf;border-top:0;border-radius:0 0 5px 5px;float:right;overflow:hidden}.message-system-unread .message-system-inner{float:none}.message-system-list{margin:0;padding:0;list-style:none;float:left}.message-system .message-system-list{width:75%}.message-system-list li{padding:5px 13px 7px 36px;position:relative}.message-system-short{padding:5px 13px 7px;float:right}.message-system-short span{display:inline-block;margin-left:7px;border-left:1px #d1ccc3 solid}.message-system-short span:first-child{border:0;margin-left:0}.message-system-short a{padding-left:27px;position:relative;height:16px}.message-system .message-system-short a:before,.message-system-list li:before{font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;height:16px;width:16px;font-size:16px;line-height:16px;text-align:center;position:absolute;left:7px;top:2px}.message-system-list li:before{top:5px;left:13px}.message-system .message-system-short .warning a:before,.message-system-list li.warning:before{content:"\e006";color:#f2a825}.message-system .message-system-short .error a:before,.message-system-list li.error:before{content:"\e086";font-family:'MUI-Icons';color:#c00815}.ui-dialog .message-system-list{margin-bottom:25px}.sales-order-create-index .order-errors .notice{color:#ed4f2e;font-size:11px;margin:5px 0 0}.order-errors .fieldset-wrapper-title .title{box-sizing:border-box;background:#fffbf0;border:1px solid #d87e34;border-radius:5px;color:#676056;font-size:14px;margin:20px 0;padding:10px 26px 10px 35px;position:relative}.order-errors .fieldset-wrapper-title .title:before{position:absolute;left:11px;top:50%;margin-top:-11px;width:auto;height:auto;font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;font-size:16px;line-height:inherit;content:'\e046';color:#d87e34}.search-global.miniform{position:relative;z-index:1000;display:inline-block;vertical-align:top;margin:6px 10px 0}.search-global.miniform .mage-suggest{border:0;border-radius:0}.search-global-actions{display:none}.search-global-field{margin:0}.search-global-field .label{position:absolute;right:4px;z-index:2;cursor:pointer;display:inline-block;text-decoration:none}.search-global-field .label>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.search-global-field .label>span.focusable:active,.search-global-field .label>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.search-global-field .label>span.focusable:active,.search-global-field .label>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.search-global-field .label:before{font-family:'MUI-Icons';content:"\e01f";font-size:18px;line-height:29px;color:#cac3b4;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.search-global-field .control{width:48px;overflow:hidden;opacity:0;transition:all .3s ease}.search-global-field .control input[type=text]{background:0 0;border:none;width:100%}.search-global-field.active{z-index:2}.search-global-field.active .label:before{display:none}.search-global-field.active .control{overflow:visible;opacity:1;transition:all .3s ease;width:300px}.search-global-menu{box-sizing:border-box;display:block;width:100%}.notifications-summary{display:inline-block;text-align:left;position:relative;z-index:1}.notifications-summary.active{z-index:999}.notifications-action{color:#f2ebde;padding:12px 22px 11px;text-transform:capitalize;display:inline-block;text-decoration:none}.notifications-action:before{font-family:"MUI-Icons";content:"\e06e";font-size:18px;line-height:18px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-action:visited,.notifications-action:focus,.notifications-action:active,.notifications-action:hover{color:#f2ebde;text-decoration:none}.notifications-action.active{background-color:#fff;color:#676056}.notifications-action .text{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-action .text.focusable:active,.notifications-action .text.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-action .text.focusable:active,.notifications-action .text.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-action .qty.counter{display:inline-block;background:#ed4f2e;color:#f2ebde;font-size:12px;line-height:12px;font-weight:700;padding:1px 3px;position:absolute;top:6px;left:50%;border-radius:4px}.notifications-list{width:300px;padding:0;margin:0}.notifications-list .last{padding:10px;text-align:center;font-size:12px}.notifications-summary .notifications-entry{padding:15px;color:#676056;font-size:11px;font-weight:400}.notifications-entry{position:relative;z-index:1}.notifications-entry:hover .action{display:block}.notifications-entry-title{padding-right:15px;color:#ed4f2e;font-size:12px;font-weight:600;display:block;margin-bottom:10px}.notifications-entry-description{line-height:1.3;display:block;max-height:3.9em;overflow:hidden;margin-bottom:10px;text-overflow:ellipsis}.notifications-close.action{position:absolute;z-index:1;top:12px;right:12px;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400;display:none}.notifications-close.action>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-close.action>span.focusable:active,.notifications-close.action>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-close.action>span.focusable:active,.notifications-close.action>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-close.action:before{font-family:'MUI-Icons';content:"\e07f";font-size:16px;line-height:inherit;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-close.action:focus,.notifications-close.action:active{background:0 0;border:none}.notifications-close.action:hover{background:0 0;border:none}.notifications-close.action.disabled,.notifications-close.action[disabled],fieldset[disabled] .notifications-close.action{cursor:not-allowed;pointer-events:none;opacity:.5}.notifications-dialog-content{display:none}.notifications-critical .notifications-entry-title{padding-left:25px;display:inline-block;text-decoration:none}.notifications-critical .notifications-entry-title:before{font-family:'MUI-Icons';content:"\e086";font-size:18px;line-height:18px;color:#c00815;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-critical .notifications-entry-title:before{position:absolute;margin-left:-25px}.notifications-dialog-content .notifications-entry-time{color:#8c867e;font-size:13px;font-family:Helvetica,Arial,sans-serif;position:absolute;right:17px;bottom:27px;text-align:right}.notifications-url{display:inline-block;text-decoration:none}.notifications-url>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-url>span.focusable:active,.notifications-url>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-url>span.focusable:active,.notifications-url>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-url:after{font-family:'MUI-Icons';content:"\e084";font-size:16px;line-height:inherit;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-2px 0 0 10px}.notifications-dialog-content .notifications-entry-title{font-size:15px}.locale-switcher-field{white-space:nowrap;float:left}.locale-switcher-field .control,.locale-switcher-field .label{vertical-align:middle;margin:0 10px 0 0;display:inline-block}.locale-switcher-select{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;border:1px solid #ada89e;max-width:200px;height:31px;background:url("../images/select-bg.svg") no-repeat 100% 50%;background-size:30px 60px;padding-right:29px;text-indent:.01px;text-overflow:''}.locale-switcher-select::-ms-expand{display:none}.lt-ie10 .locale-switcher-select{background-image:none;padding-right:4px}@-moz-document url-prefix(){.locale-switcher-select{background-image:none}}@-moz-document url-prefix(){.locale-switcher-select{background-image:none}}.mage-suggest{text-align:left;box-sizing:border-box;position:relative;display:inline-block;vertical-align:top;width:100%;background-color:#fff;border:1px solid #ada89e;border-radius:2px}.mage-suggest:after{position:absolute;top:3px;right:3px;bottom:0;width:22px;text-align:center;font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;content:'\e01f';font-size:18px;color:#b2b2b2}.mage-suggest input[type=search],.mage-suggest input.search{width:100%;border:none;background:0 0;padding-right:30px}.mage-suggest.category-select input[type=search],.mage-suggest.category-select input.search{height:26px}.mage-suggest-dropdown{position:absolute;left:0;right:0;top:100%;margin:1px -1px 0;border:1px solid #cac2b5;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);z-index:990}.mage-suggest-dropdown ul{margin:0;padding:0;list-style:none}.mage-suggest-dropdown li{border-bottom:1px solid #e5e5e5;padding:0}.mage-suggest-dropdown li a{display:block}.mage-suggest-dropdown li a.ui-state-focus{background:#f5f5f5}.mage-suggest-dropdown li a,.mage-suggest-dropdown .jstree li a:hover,.mage-suggest-dropdown .jstree .jstree-hovered,.mage-suggest-dropdown .jstree .jstree-clicked{padding:6px 12px 5px;text-decoration:none;color:#333}.mage-suggest-dropdown .jstree li a:hover,.mage-suggest-dropdown .jstree .jstree-hovered,.mage-suggest-dropdown .jstree .jstree-clicked{border:none}.mage-suggest-dropdown .jstree li{border-bottom:0}.mage-suggest-dropdown .jstree li a{display:inline-block}.mage-suggest-dropdown .jstree .mage-suggest-selected>a{color:#000;background:#f1ffeb}.field-category_ids .mage-suggest-dropdown,.field-new_category_parent .mage-suggest-dropdown{max-height:200px;overflow:auto}.mage-suggest-dropdown .jstree .mage-suggest-selected>a:hover,.mage-suggest-dropdown .jstree .mage-suggest-selected>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-selected>.jstree-clicked,.mage-suggest-dropdown .jstree .mage-suggest-selected.mage-suggest-not-active>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-selected.mage-suggest-not-active>.jstree-clicked{background:#e5ffd9}.mage-suggest-dropdown .jstree .mage-suggest-not-active>a{color:#d4d4d4}.mage-suggest-dropdown .jstree .mage-suggest-not-active>a:hover,.mage-suggest-dropdown .jstree .mage-suggest-not-active>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-not-active>.jstree-clicked{background:#f5f5f5}.mage-suggest-dropdown .category-path{font-size:11px;margin-left:10px;color:#9ba8b5}.suggest-expandable .action-dropdown .action-toggle{display:inline-block;max-width:500px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:none;box-shadow:none;color:#676056;font-size:12px;padding:5px 4px;filter:none}.suggest-expandable .action-dropdown .action-toggle span{display:inline}.suggest-expandable .action-dropdown .action-toggle:before{display:inline-block;float:right;margin-left:4px;font-size:13px;color:#b2b0ad}.suggest-expandable .action-dropdown .action-toggle:hover:before{color:#7e7e7e}.suggest-expandable .dropdown-menu{margin:1px 0 0;left:0;right:auto;width:245px;z-index:4}.suggest-expandable .mage-suggest{border:none;border-radius:3px 3px 0 0}.suggest-expandable .mage-suggest:after{top:10px;right:8px}.suggest-expandable .mage-suggest-inner .title{margin:0;padding:0 10px 4px;text-transform:uppercase;color:#a6a098;font-size:12px;border-bottom:1px solid #e5e5e5}.suggest-expandable .mage-suggest-inner>input[type=search],.suggest-expandable .mage-suggest-inner>input.search{position:relative;margin:6px 5px 5px;padding-right:20px;border:1px solid #ada89e;width:236px;z-index:1}.suggest-expandable .mage-suggest-inner>input.ui-autocomplete-loading,.suggest-expandable .mage-suggest-inner>input.mage-suggest-state-loading{background:#fff url("../mui/images/ajax-loader-small.gif") no-repeat 190px 50%}.suggest-expandable .mage-suggest-dropdown{margin-top:0;border-top:0;border-radius:0 0 3px 3px;max-height:300px;overflow:auto;width:100%;float:left}.suggest-expandable .mage-suggest-dropdown ul{margin:0;padding:0;list-style:none}.suggest-expandable .action-show-all:hover,.suggest-expandable .action-show-all:active,.suggest-expandable .action-show-all:focus,.suggest-expandable .action-show-all[disabled]{border-top:1px solid #e5e5e5;display:block;width:100%;padding:8px 10px 10px;text-align:left;font:12px/1.333 Arial,Verdana,sans-serif;color:#676056}.product-actions .suggest-expandable{max-width:500px;float:left;margin-top:1px}.page-actions.fixed #product-template-suggest-container{display:none}.catalog-category-edit .col-2-left-layout:before{display:none}.category-content .ui-tabs-panel .fieldset{padding-top:40px}.category-content .ui-tabs-panel .fieldset .legend{display:none}.attributes-edit-form .field:not(.field-weight) .addon{display:block;position:relative}.attributes-edit-form .field:not(.field-weight) .addon input[type=text]{border-width:1px}.attributes-edit-form .field:not(.field-weight) .addon .addafter{display:block;border:0;height:auto;width:auto}.attributes-edit-form .field:not(.field-weight) .addon input:focus~.addafter{box-shadow:none}.attributes-edit-form .with-addon .textarea{margin:0}.attributes-edit-form .attribute-change-checkbox{display:block;margin-top:5px}.attributes-edit-form .attribute-change-checkbox .label{float:none;padding:0;width:auto}.attributes-edit-form .attribute-change-checkbox .checkbox{margin-right:5px;width:auto}.attributes-edit-form .field-price .addon>input,.attributes-edit-form .field-special_price .addon>input,.attributes-edit-form .field-gift_wrapping_price .addon>input,.attributes-edit-form .field-msrp .addon>input,.attributes-edit-form .field-gift_wrapping_price .addon>input{padding-left:23px}.attributes-edit-form .field-price .addafter>strong,.attributes-edit-form .field-special_price .addafter>strong,.attributes-edit-form .field-gift_wrapping_price .addafter>strong,.attributes-edit-form .field-msrp .addafter>strong,.attributes-edit-form .field-gift_wrapping_price .addafter>strong{left:5px;position:absolute;top:3px}.attributes-edit-form .field.type-price input:focus+label,.attributes-edit-form .field-price input:focus+label,.attributes-edit-form .field-special_price input:focus+label,.attributes-edit-form .field-msrp input:focus+label,.attributes-edit-form .field-weight input:focus+label{box-shadow:none}.attributes-edit-form .field-special_from_date>.control .input-text,.attributes-edit-form .field-special_to_date>.control .input-text,.attributes-edit-form .field-news_from_date>.control .input-text,.attributes-edit-form .field-news_to_date>.control .input-text,.attributes-edit-form .field-custom_design_from>.control .input-text,.attributes-edit-form .field-custom_design_to>.control .input-text{border-width:1px;width:130px}.attributes-edit-form .field-weight .fields-group-2 .control{padding-right:27px}.attributes-edit-form .field-weight .fields-group-2 .control .addafter+.addafter{border-width:1px 1px 1px 0;border-style:solid;height:28px;right:0;position:absolute;top:0}.attributes-edit-form .field-weight .fields-group-2 .control .addafter strong{line-height:28px}.attributes-edit-form .field-weight .fields-group-2 .control>input:focus+.addafter+.addafter{box-shadow:0 0 8px rgba(82,168,236,.6)}.attributes-edit-form .field-gift_message_available .addon>input[type=checkbox],.attributes-edit-form .field-gift_wrapping_available .addon>input[type=checkbox]{width:auto;margin-right:5px}.attributes-edit-form .fieldset>.addafter{display:none}.advanced-inventory-edit .field.choice{display:block;margin:3px 0 0}.advanced-inventory-edit .field.choice .label{padding-top:1px}.product-actions:before,.product-actions:after{content:"";display:table}.product-actions:after{clear:both}.product-actions:before,.product-actions:after{content:"";display:table}.product-actions:after{clear:both}.product-actions .switcher{float:right}#configurable-attributes-container .actions-select{display:inline-block;position:relative}#configurable-attributes-container .actions-select:before,#configurable-attributes-container .actions-select:after{content:"";display:table}#configurable-attributes-container .actions-select:after{clear:both}#configurable-attributes-container .actions-select:before,#configurable-attributes-container .actions-select:after{content:"";display:table}#configurable-attributes-container .actions-select:after{clear:both}#configurable-attributes-container .actions-select .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}#configurable-attributes-container .actions-select .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:22px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#configurable-attributes-container .actions-select .action.toggle:hover:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle:active:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle.active{display:inline-block;text-decoration:none}#configurable-attributes-container .actions-select .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:22px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#configurable-attributes-container .actions-select .action.toggle.active:hover:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle.active:active:after{color:inherit}#configurable-attributes-container .actions-select ul.dropdown{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:100%;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}#configurable-attributes-container .actions-select ul.dropdown li{margin:0;padding:3px 5px}#configurable-attributes-container .actions-select ul.dropdown li:hover{background:#e8e8e8;cursor:pointer}#configurable-attributes-container .actions-select.active{overflow:visible}#configurable-attributes-container .actions-select.active ul.dropdown{display:block}#configurable-attributes-container .actions-select .action.toggle{padding:1px 8px;border:1px solid #ada89e;background:#fff;border-radius:0 2px 2px 0}#configurable-attributes-container .actions-select .action.toggle:after{width:14px;text-indent:-2px}#configurable-attributes-container .actions-select ul.dropdown li:hover{background:#eef8fc}#configurable-attributes-container .actions-select ul.dropdown a{color:#333;text-decoration:none}#product-variations-matrix .actions-image-uploader{display:inline-block;position:relative;display:block;width:50px}#product-variations-matrix .actions-image-uploader:before,#product-variations-matrix .actions-image-uploader:after{content:"";display:table}#product-variations-matrix .actions-image-uploader:after{clear:both}#product-variations-matrix .actions-image-uploader:before,#product-variations-matrix .actions-image-uploader:after{content:"";display:table}#product-variations-matrix .actions-image-uploader:after{clear:both}#product-variations-matrix .actions-image-uploader .action.split{float:left;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle{float:right;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle{padding:6px 5px;display:inline-block;text-decoration:none}#product-variations-matrix .actions-image-uploader .action.toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle:hover:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle:active:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle.active{display:inline-block;text-decoration:none}#product-variations-matrix .actions-image-uploader .action.toggle.active>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle.active:hover:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle.active:active:after{color:inherit}#product-variations-matrix .actions-image-uploader ul.dropdown{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:100%;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}#product-variations-matrix .actions-image-uploader ul.dropdown li{margin:0;padding:3px 5px}#product-variations-matrix .actions-image-uploader ul.dropdown li:hover{background:#e8e8e8;cursor:pointer}#product-variations-matrix .actions-image-uploader.active{overflow:visible}#product-variations-matrix .actions-image-uploader.active ul.dropdown{display:block}#product-variations-matrix .actions-image-uploader .action.toggle{padding:0 2px;border:1px solid #b7b2a7;background:#fff;border-radius:0 4px 4px 0;border-left:none;height:33px}#product-variations-matrix .actions-image-uploader .action.toggle.no-display{display:none}#product-variations-matrix .actions-image-uploader .action.toggle:after{width:12px;text-indent:-5px}#product-variations-matrix .actions-image-uploader ul.dropdown{left:0;margin-left:0;width:100px}#product-variations-matrix .actions-image-uploader ul.dropdown li:hover{background:#eef8fc}#product-variations-matrix .actions-image-uploader ul.dropdown a{color:#333;text-decoration:none}.debugging-hints .page-actions{position:relative;z-index:1}.debugging-hints .page-actions .debugging-hint-template-file{left:auto !important;right:0 !important}.filter-segments{list-style:none;padding:0}.adminhtml-report-customer-test-detail .col-id{width:35px}.adminhtml-report-customer-test-detail .col-period{white-space:nowrap;width:70px}.adminhtml-report-customer-test-detail .col-zip{width:50px}.adminhtml-report-customer-test-segment .col-id{width:35px}.adminhtml-report-customer-test-segment .col-status{width:65px}.adminhtml-report-customer-test-segment .col-qty{width:145px}.adminhtml-report-customer-test-segment .col-segment,.adminhtml-report-customer-test-segment .col-website{width:35%}.adminhtml-report-customer-test-segment .col-select{width:45px}.test-custom-attributes{margin-bottom:20px}.adminhtml-test-index th.col-id{text-align:left}.adminhtml-test-index .col-price{text-align:right;width:50px}.adminhtml-test-index .col-actions{width:50px}.adminhtml-test-index .col-select{width:60px}.adminhtml-test-edit .field-image .control{line-height:28px}.adminhtml-test-edit .field-image a{display:inline-block;margin:0 5px 0 0}.adminhtml-test-edit .field-image img{vertical-align:middle}.adminhtml-test-new .field-image .input-file,.adminhtml-test-edit .field-image .input-file{display:inline-block;margin:0 15px 0 0;width:auto}.adminhtml-test-new .field-image .addafter,.adminhtml-test-edit .field-image .addafter{border:0;box-shadow:none;display:inline-block;margin:0 15px 0 0;height:auto;width:auto}.adminhtml-test-new .field-image .delete-image,.adminhtml-test-edit .field-image .delete-image{display:inline-block;white-space:nowrap}.adminhtml-test-edit .field-image .delete-image input{margin:-3px 5px 0 0;width:auto;display:inline-block}.adminhtml-test-edit .field-image .addon .delete-image input:focus+label{border:0;box-shadow:none}.adminhtml-test-index .col-id{width:35px}.adminhtml-test-index .col-status{white-space:normal;width:75px}.adminhtml-test-index .col-websites{white-space:nowrap;width:200px}.adminhtml-test-index .col-price .label{display:inline-block;min-width:60px;white-space:nowrap}.adminhtml-test-index .col-price .price-excl-tax .price,.adminhtml-test-index .col-price .price-incl-tax .price{font-weight:700}.invitee_information,.inviter_information{width:48.9362%}.invitee_information{float:left}.inviter_information{float:right}.test_information .data-table th,.invitee_information .data-table th,.inviter_information .data-table th{width:20%;white-space:nowrap}.test_information .data-table textarea,.test_information .data-table input{width:100%}.tests-history ul{margin:0;padding-left:25px}.tests-history ul .status:before{display:inline-block;content:"|";margin:0 10px}.adminhtml-report-test-order .col-period{white-space:nowrap;width:70px}.adminhtml-report-test-order .col-inv-sent,.adminhtml-report-test-order .col-inv-acc,.adminhtml-report-test-order .col-acc,.adminhtml-report-test-order .col-rate{text-align:right;width:23%}.adminhtml-report-test-customer .col-id{width:35px}.adminhtml-report-test-customer .col-period{white-space:nowrap;width:70px}.adminhtml-report-test-customer .col-inv-sent,.adminhtml-report-test-customer .col-inv-acc{text-align:right;width:120px}.adminhtml-report-test-index .col-period{white-space:nowrap}.adminhtml-report-test-index .col-inv-sent,.adminhtml-report-test-index .col-inv-acc,.adminhtml-report-test-index .col-inv-disc,.adminhtml-report-test-index .col-inv-acc-rate,.adminhtml-report-test-index .col-inv-disc-rate{text-align:right;width:19%}.test_information .data-table,.invitee_information .data-table,.inviter_information .data-table{width:100%}.test_information .data-table tbody tr th,.invitee_information .data-table tbody tr th,.inviter_information .data-table tbody tr th{font-weight:700}.test_information .data-table tbody tr td,.test_information .data-table tbody tr th,.invitee_information .data-table tbody tr td,.invitee_information .data-table tbody tr th,.inviter_information .data-table tbody tr td,.inviter_information .data-table tbody tr th{background-color:#fff;border:0;padding:9px 10px 10px;color:#666;vertical-align:top}.test_information .data-table tbody tr:nth-child(2n+1) td,.test_information .data-table tbody tr:nth-child(2n+1) th,.invitee_information .data-table tbody tr:nth-child(2n+1) td,.invitee_information .data-table tbody tr:nth-child(2n+1) th,.inviter_information .data-table tbody tr:nth-child(2n+1) td,.inviter_information .data-table tbody tr:nth-child(2n+1) th{background-color:#fbfaf6}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table .col-sort-order{width:80px}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td{vertical-align:top}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td select,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td select{display:block;width:100%}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td .input-radio.global-scope,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td .input-radio.global-scope{margin-top:9px}.sales-order-create-index .ui-dialog .content>.test .field.text .input-text{width:100%}.sales-order-create-index .ui-dialog .content>.test .note .price{font-weight:600}.sales-order-create-index .ui-dialog .content>.test .note .price:before{content:": "}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .label:after{content:": "}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .control{display:inline-block;font-weight:600}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .control .control-value{margin:-2px 0 0;padding:0}.eq-ie9 [class^=" adminhtml-test-"] .custom-options .data-table{word-wrap:normal;table-layout:auto}.rma-items .col-actions a.disabled,.newRma .col-actions a.disabled{cursor:default;opacity:.5}.rma-items .col-actions a.disabled:hover,.newRma .col-actions a.disabled:hover{text-decoration:none}.block.mselect-list .mselect-input{width:100%}.block.mselect-list .mselect-input-container .mselect-save{top:4px}.block.mselect-list .mselect-input-container .mselect-cancel{top:4px}html{font-size:62.5%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-size-adjust:100%}body,html{height:100%;min-height:100%}body{color:#676056;font-family:'Open Sans',sans-serif;line-height:1.33;font-weight:400;font-size:1.4rem;background:#f2ebde;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}body>*{-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0;-webkit-flex-basis:auto;flex-basis:auto}.page-wrapper{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-height:100%;width:100%;max-width:100%;min-width:990px}.page-wrapper>*{-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0;-webkit-flex-basis:auto;flex-basis:auto}.page-header{text-align:right}.page-header-wrapper{background-color:#31302b}.page-header:after{content:"";display:table;clear:both}.page-header .logo{margin-top:5px;float:left;text-decoration:none;display:inline-block}.page-header .logo:before{content:"";display:inline-block;vertical-align:middle;width:109px;height:35px;background-image:url("../images/logo.svg");background-size:109px 70px;background-repeat:no-repeat}.page-header .logo:after{display:inline-block;vertical-align:middle;margin-left:10px;content:attr(data-edition);font-weight:600;font-size:16px;color:#ef672f;margin-top:-2px}.page-header .logo span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.page-header .logo span.focusable:active,.page-header .logo span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.page-header .logo span.focusable:active,.page-header .logo span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.page-header .dropdown-menu{border:0}.admin-user{display:inline-block;vertical-align:top;position:relative;text-align:left}.admin-user-account{text-decoration:none;display:inline-block;padding:12px 14px;color:#f2ebde}.admin-user-account:after{font-family:"MUI-Icons";content:"\e02c";font-size:13px;line-height:13px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-3px 0 0}.admin-user-account:link,.admin-user-account:visited{color:#f2ebde}.admin-user-account:focus,.admin-user-account:active,.admin-user-account:hover{color:#f2ebde;text-decoration:none}.active .admin-user-account{background-color:#fff;color:#676056}.admin-user-menu{padding:15px;white-space:nowrap;margin-top:0}.admin-user-menu li{border:0;padding:0}.admin-user-menu li:hover{background:0 0}.admin-user-menu a{display:block;color:#676056;font-size:13px;font-weight:400;line-height:1.385;padding:3px 12px 3px;text-decoration:none}.admin-user-menu a:focus,.admin-user-menu a:hover{text-decoration:underline}.admin-user-menu a:hover{color:#fff;background:#989287;text-decoration:none}.admin-user-menu a span:before{content:"("}.admin-user-menu a span:after{content:")"}.page-actions.fixed .page-actions-buttons{padding-right:15px}.page-main-actions{background:#e0dace;color:#645d53;padding:15px;margin-left:auto;margin-right:auto;box-sizing:border-box}.page-main-actions:before,.page-main-actions:after{content:"";display:table}.page-main-actions:after{clear:both}.page-main-actions:before,.page-main-actions:after{content:"";display:table}.page-main-actions:after{clear:both}.page-main-actions .page-actions{float:right}.page-main-actions .page-actions .page-actions-buttons{float:right;display:-webkit-flex;display:-ms-flexbox;display:flex;justify-content:flex-end}.page-main-actions .page-actions button,.page-main-actions .page-actions .action-add.mselect-button-add{margin-left:13px}.page-main-actions .page-actions button.primary,.page-main-actions .page-actions .action-add.mselect-button-add.primary{float:right;-ms-flex-order:2;-webkit-order:2;order:2}.page-main-actions .page-actions button.save:not(.primary),.page-main-actions .page-actions .action-add.mselect-button-add.save:not(.primary){float:right;-ms-flex-order:1;-webkit-order:1;order:1}.page-main-actions .page-actions button.back,.page-main-actions .page-actions button.action-back,.page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.back,.page-main-actions .page-actions .action-add.mselect-button-add.action-back,.page-main-actions .page-actions .action-add.mselect-button-add.delete{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400;margin:0 13px}.page-main-actions .page-actions button.back:focus,.page-main-actions .page-actions button.action-back:focus,.page-main-actions .page-actions button.delete:focus,.page-main-actions .page-actions button.back:active,.page-main-actions .page-actions button.action-back:active,.page-main-actions .page-actions button.delete:active,.page-main-actions .page-actions .action-add.mselect-button-add.back:focus,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:focus,.page-main-actions .page-actions .action-add.mselect-button-add.delete:focus,.page-main-actions .page-actions .action-add.mselect-button-add.back:active,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:active,.page-main-actions .page-actions .action-add.mselect-button-add.delete:active{background:0 0;border:none}.page-main-actions .page-actions button.back:hover,.page-main-actions .page-actions button.action-back:hover,.page-main-actions .page-actions button.delete:hover,.page-main-actions .page-actions .action-add.mselect-button-add.back:hover,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:hover,.page-main-actions .page-actions .action-add.mselect-button-add.delete:hover{background:0 0;border:none}.page-main-actions .page-actions button.back.disabled,.page-main-actions .page-actions button.action-back.disabled,.page-main-actions .page-actions button.delete.disabled,.page-main-actions .page-actions button.back[disabled],.page-main-actions .page-actions button.action-back[disabled],.page-main-actions .page-actions button.delete[disabled],fieldset[disabled] .page-main-actions .page-actions button.back,fieldset[disabled] .page-main-actions .page-actions button.action-back,fieldset[disabled] .page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.back.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.action-back.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.delete.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.back[disabled],.page-main-actions .page-actions .action-add.mselect-button-add.action-back[disabled],.page-main-actions .page-actions .action-add.mselect-button-add.delete[disabled],fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.back,fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.action-back,fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.delete{cursor:not-allowed;pointer-events:none;opacity:.5}.ie .page-main-actions .page-actions button.back,.ie .page-main-actions .page-actions button.action-back,.ie .page-main-actions .page-actions button.delete,.ie .page-main-actions .page-actions .action-add.mselect-button-add.back,.ie .page-main-actions .page-actions .action-add.mselect-button-add.action-back,.ie .page-main-actions .page-actions .action-add.mselect-button-add.delete{margin-top:6px}.page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.delete{color:#e22626;float:left;-ms-flex-order:-1;-webkit-order:-1;order:-1}.page-main-actions .page-actions button.back,.page-main-actions .page-actions button.action-back,.page-main-actions .page-actions .action-add.mselect-button-add.back,.page-main-actions .page-actions .action-add.mselect-button-add.action-back{float:left;-ms-flex-order:-1;-webkit-order:-1;order:-1;display:inline-block;text-decoration:none}.page-main-actions .page-actions button.back:before,.page-main-actions .page-actions button.action-back:before,.page-main-actions .page-actions .action-add.mselect-button-add.back:before,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:before{font-family:'icons-blank-theme';content:'\e625';font-size:inherit;line-height:normal;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:0 2px 0 0}.page-main-actions .page-actions .actions-split{margin-left:13px;float:right;-ms-flex-order:2;-webkit-order:2;order:2}.page-main-actions .page-actions .actions-split button.primary,.page-main-actions .page-actions .actions-split .action-add.mselect-button-add.primary{float:left}.page-main-actions .page-actions .actions-split .dropdown-menu{text-align:left}.page-main-actions .page-actions .actions-split .dropdown-menu .item{display:block}.page-main-actions .page-actions.fixed{position:fixed;top:0;left:0;right:0;z-index:10;padding:0;background:-webkit-linear-gradient(top,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:-ms-linear-gradient(top,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:linear-gradient(to bottom,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:#e0dace}.page-main-actions .page-actions.fixed .page-actions-inner{position:relative;padding-top:15px;padding-bottom:15px;min-height:36px;text-align:right;box-sizing:border-box}.page-main-actions .page-actions.fixed .page-actions-inner:before,.page-main-actions .page-actions.fixed .page-actions-inner:after{content:"";display:table}.page-main-actions .page-actions.fixed .page-actions-inner:after{clear:both}.page-main-actions .page-actions.fixed .page-actions-inner:before,.page-main-actions .page-actions.fixed .page-actions-inner:after{content:"";display:table}.page-main-actions .page-actions.fixed .page-actions-inner:after{clear:both}.page-main-actions .page-actions.fixed .page-actions-inner:before{text-align:left;content:attr(data-title);float:left;font-size:20px;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lt-ie10 .page-main-actions .page-actions.fixed .page-actions-inner{background:#f5f2ed}.page-main-actions .store-switcher{margin-top:5px}.store-switcher{display:inline-block;font-size:13px}.store-switcher .label{margin-right:5px}.store-switcher .actions.dropdown{display:inline-block;position:relative}.store-switcher .actions.dropdown:before,.store-switcher .actions.dropdown:after{content:"";display:table}.store-switcher .actions.dropdown:after{clear:both}.store-switcher .actions.dropdown:before,.store-switcher .actions.dropdown:after{content:"";display:table}.store-switcher .actions.dropdown:after{clear:both}.store-switcher .actions.dropdown .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}.store-switcher .actions.dropdown .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:20px;color:#645d53;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.store-switcher .actions.dropdown .action.toggle:hover:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle:active:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle.active{display:inline-block;text-decoration:none}.store-switcher .actions.dropdown .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:20px;color:#645d53;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.store-switcher .actions.dropdown .action.toggle.active:hover:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle.active:active:after{color:#645d53}.store-switcher .actions.dropdown .dropdown-menu{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px #ada89e solid;position:absolute;z-index:100;top:100%;min-width:195px;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}.store-switcher .actions.dropdown .dropdown-menu li{margin:0;padding:0}.store-switcher .actions.dropdown .dropdown-menu li:hover{background:0 0;cursor:pointer}.store-switcher .actions.dropdown.active{overflow:visible}.store-switcher .actions.dropdown.active .dropdown-menu{display:block}.store-switcher .actions.dropdown .action.toggle{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400;color:#026294;line-height:normal;margin-top:2px;vertical-align:middle}.store-switcher .actions.dropdown .action.toggle:focus,.store-switcher .actions.dropdown .action.toggle:active{background:0 0;border:none}.store-switcher .actions.dropdown .action.toggle:hover{background:0 0;border:none}.store-switcher .actions.dropdown .action.toggle.disabled,.store-switcher .actions.dropdown .action.toggle[disabled],fieldset[disabled] .store-switcher .actions.dropdown .action.toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.store-switcher .actions.dropdown ul.dropdown-menu{margin-top:4px;padding-top:5px;left:0}.store-switcher .actions.dropdown ul.dropdown-menu li{border:0;cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li:hover{cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li a,.store-switcher .actions.dropdown ul.dropdown-menu li span{padding:5px 13px;display:block;color:#645d53}.store-switcher .actions.dropdown ul.dropdown-menu li a{text-decoration:none}.store-switcher .actions.dropdown ul.dropdown-menu li a:hover{background:#edf9fb}.store-switcher .actions.dropdown ul.dropdown-menu li span{color:#ababab;cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li.current span{color:#645d53;background:#eee}.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store a,.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store span{padding-left:26px}.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store-view a,.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store-view span{padding-left:39px}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar{border-top:1px #ededed solid;margin-top:10px}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar a{display:inline-block;text-decoration:none;display:block}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar a:before{font-family:'icons-blank-theme';content:'\e606';font-size:20px;line-height:normal;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:text-top;text-align:center;margin:0 3px 0 -4px}.tooltip{display:inline-block;margin-left:5px}.tooltip .help span,.tooltip .help a{width:16px;height:16px;text-align:center;background:rgba(194,186,169,.5);cursor:pointer;border-radius:10px;vertical-align:middle;display:inline-block;text-decoration:none}.tooltip .help span:hover,.tooltip .help a:hover{background:#c2baa9}.tooltip .help span>span,.tooltip .help a>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.tooltip .help span>span.focusable:active,.tooltip .help a>span.focusable:active,.tooltip .help span>span.focusable:focus,.tooltip .help a>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.tooltip .help span>span.focusable:active,.tooltip .help a>span.focusable:active,.tooltip .help span>span.focusable:focus,.tooltip .help a>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.tooltip .help span:before,.tooltip .help a:before{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;content:'?';font-size:13px;line-height:16px;color:#5a534a;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center}.tooltip .help span:before,.tooltip .help a:before{font-weight:700}.tooltip .tooltip-content{display:none;position:absolute;max-width:200px;margin-top:10px;margin-left:-19px;padding:4px 8px;border-radius:3px;background:#000;background:rgba(49,48,43,.8);color:#fff;text-shadow:none;z-index:20}.tooltip .tooltip-content:before{content:'';position:absolute;width:0;height:0;top:-5px;left:20px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000;opacity:.8}.tooltip .tooltip-content.loading{position:absolute}.tooltip .tooltip-content.loading:before{border-bottom-color:rgba(0,0,0,.3)}.tooltip:hover>.tooltip-content{display:block}button,.action-add.mselect-button-add{border-radius:2px;background-image:none;background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:0;vertical-align:middle}button:focus,button:active,.action-add.mselect-button-add:focus,.action-add.mselect-button-add:active{background:#cac3b4;border:1px solid #989287}button:hover,.action-add.mselect-button-add:hover{background:#cac3b4}button.disabled,button[disabled],fieldset[disabled] button,.action-add.mselect-button-add.disabled,.action-add.mselect-button-add[disabled],fieldset[disabled] .action-add.mselect-button-add{cursor:default;pointer-events:none;opacity:.5}button.primary,.action-add.mselect-button-add.primary{background-image:none;background:#007dbd;padding:6px 13px;color:#fff;border:1px solid #0a6c9f;cursor:pointer;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;box-sizing:border-box;vertical-align:middle}button.primary:focus,button.primary:active,.action-add.mselect-button-add.primary:focus,.action-add.mselect-button-add.primary:active{background:#026294;border:1px solid #004c74;color:#fff}button.primary:hover,.action-add.mselect-button-add.primary:hover{background:#026294;border:1px solid #026294}button.primary.disabled,button.primary[disabled],fieldset[disabled] button.primary,.action-add.mselect-button-add.primary.disabled,.action-add.mselect-button-add.primary[disabled],fieldset[disabled] .action-add.mselect-button-add.primary{cursor:default;pointer-events:none;opacity:.5}.actions-split{display:inline-block;position:relative;vertical-align:middle}.actions-split button,.actions-split .action-add.mselect-button-add{margin-left:0!important}.actions-split:before,.actions-split:after{content:"";display:table}.actions-split:after{clear:both}.actions-split:before,.actions-split:after{content:"";display:table}.actions-split:after{clear:both}.actions-split .action-default{float:left;margin:0}.actions-split .action-toggle{float:right;margin:0}.actions-split button.action-default,.actions-split .action-add.mselect-button-add.action-default{border-top-right-radius:0;border-bottom-right-radius:0}.actions-split button+.action-toggle,.actions-split .action-add.mselect-button-add+.action-toggle{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.actions-split .action-toggle{padding:6px 5px;display:inline-block;text-decoration:none}.actions-split .action-toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.actions-split .action-toggle>span.focusable:active,.actions-split .action-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle>span.focusable:active,.actions-split .action-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.actions-split .action-toggle:hover:after{color:inherit}.actions-split .action-toggle:active:after{color:inherit}.actions-split .action-toggle.active{display:inline-block;text-decoration:none}.actions-split .action-toggle.active>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.actions-split .action-toggle.active>span.focusable:active,.actions-split .action-toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle.active>span.focusable:active,.actions-split .action-toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.actions-split .action-toggle.active:hover:after{color:inherit}.actions-split .action-toggle.active:active:after{color:inherit}.actions-split .dropdown-menu{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:175px;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}.actions-split .dropdown-menu li{margin:0;padding:3px 5px}.actions-split .dropdown-menu li:hover{background:#e8e8e8;cursor:pointer}.actions-split .dropdown-menu:before,.actions-split .dropdown-menu:after{content:"";position:absolute;display:block;width:0;height:0;border-bottom-style:solid}.actions-split .dropdown-menu:before{z-index:99;border:solid 6px;border-color:transparent transparent #fff}.actions-split .dropdown-menu:after{z-index:98;border:solid 7px;border-color:transparent transparent #bbb}.actions-split .dropdown-menu:before{top:-12px;right:10px}.actions-split .dropdown-menu:after{top:-14px;right:9px}.actions-split.active{overflow:visible}.actions-split.active .dropdown-menu{display:block}.actions-split .action-toggle:after{height:13px}.page-content:after{content:"";display:table;clear:both}.page-wrapper>.page-content{margin-bottom:20px}.page-footer{padding:15px 0}.page-footer-wrapper{background-color:#e0dacf;margin-top:auto}.page-footer:after{content:"";display:table;clear:both}.footer-legal{float:right;width:550px}.footer-legal .link-report,.footer-legal .magento-version,.footer-legal .copyright{font-size:13px}.footer-legal:before{content:"";display:inline-block;vertical-align:middle;position:absolute;z-index:1;margin-top:2px;margin-left:-35px;width:30px;height:35px;background-size:109px 70px;background:url("../images/logo.svg") no-repeat 0 -21px}.icon-error{margin-left:15px;color:#c00815;font-size:11px}.icon-error:before{font-family:'MUI-Icons';content:"\e086";font-size:13px;line-height:13px;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-1px 5px 0 0}.ui-widget-overlay{position:fixed}.control .nested{padding:0}.control *:first-child{margin-bottom:0}.field-tooltip{display:inline-block;vertical-align:top;margin-top:5px;position:relative;z-index:1;width:0;overflow:visible}.field-choice .field-tooltip{margin-top:10px}.field-tooltip:hover{z-index:99}.field-tooltip-action{position:relative;z-index:2;margin-left:18px;width:22px;height:22px;display:inline-block;cursor:pointer}.field-tooltip-action:before{content:"?";font-weight:500;font-size:18px;display:inline-block;overflow:hidden;height:22px;border-radius:11px;line-height:22px;width:22px;text-align:center;color:#fff;background-color:#514943}.field-tooltip-action span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.field-tooltip-action span.focusable:active,.field-tooltip-action span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.field-tooltip-action span.focusable:active,.field-tooltip-action span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text:focus+.field-tooltip-content,.field-tooltip:hover .field-tooltip-content{display:block}.field-tooltip-content{display:none;position:absolute;z-index:1;width:320px;background:#fff8d7;padding:15px 25px;right:-66px;border:1px solid #adadad;border-radius:1px;bottom:42px;box-shadow:0 2px 8px 0 rgba(0,0,0,.3)}.field-tooltip-content:after,.field-tooltip-content:before{content:"";display:block;width:0;height:0;border:16px solid transparent;border-top-color:#adadad;position:absolute;right:20px;top:100%;z-index:3}.field-tooltip-content:after{border-top-color:#fff8d7;margin-top:-1px;z-index:4}.form__field.field-error .control [class*=control-]{border-color:#e22626}.form__field.field-error .control [class*=control-]:before{border-color:#e22626}.form__field .mage-error{border:1px solid #e22626;display:block;margin:2px 0 0;padding:6px 10px 10px;background:#fff8d6;color:#555;font-size:12px;font-weight:500;box-sizing:border-box;max-width:380px}.no-flexbox.no-flexboxlegacy .form__field .control-addon+.mage-error{display:inline-block;width:100%}.form__field{position:relative;z-index:1}.form__field:hover{z-index:2}.control .form__field{position:static}.form__field[data-config-scope]:before{content:attr(data-config-scope);display:inline-block;position:absolute;color:gray;right:0;top:6px}.control .form__field[data-config-scope]:nth-child(n+2):before{content:""}.form__field.field-disabled>.label{color:#999}.form__field.field-disabled.field .control [class*=control-][disabled]{background-color:#e9e9e9;opacity:.5;color:#303030;border-color:#adadad}.control-fields .label~.control{width:100%}.form__field{border:0;padding:0}.form__field .note{color:#303030;padding:0;margin:10px 0 0;max-width:380px}.form__field .note:before{display:none}.form__field.form__field{margin-bottom:0}.form__field.form__field+.form__field.form__field{margin-top:15px}.form__field.form__field:not(.choice)~.choice{margin-left:20px;margin-top:5px}.form__field.form__field.choice~.choice{margin-top:9px}.form__field.form__field~.choice:last-child{margin-bottom:8px}.fieldset>.form__field{margin-bottom:30px}.form__field .label{color:#303030}.form__field:not(.choice)>.label{font-size:14px;font-weight:600;width:30%;padding-right:30px;padding-top:0;line-height:33px;white-space:nowrap}.form__field:not(.choice)>.label:before{content:".";visibility:hidden;width:0;margin-left:-7px;overflow:hidden}.form__field:not(.choice)>.label span{white-space:normal;display:inline-block;vertical-align:middle;line-height:1.2}.form__field.required>.label:after{content:"";margin-left:0}.form__field.required>.label span:after{content:"*";color:#eb5202;display:inline;font-weight:500;font-size:16px;margin-top:2px;position:absolute;z-index:1;margin-left:10px}.form__field .control-file{margin-top:6px}.form__field .control-select{line-height:33px}.form__field .control-select:not([multiple]),.form__field .control-text{height:33px;max-width:380px}.form__field .control-addon{max-width:380px}.form__field .control-textarea,.form__field .control-select,.form__field .control-text{border:1px solid #adadad;border-radius:1px;padding:0 10px;color:#303030;background-color:#fff;font-weight:500;font-size:15px;min-width:11em}.form__field .control-textarea:focus,.form__field .control-select:focus,.form__field .control-text:focus{outline:0;border-color:#007bdb;box-shadow:none}.form__field .control-text{line-height:auto}.form__field .control-textarea{padding-top:6px;padding-bottom:6px;line-height:1.18em}.form__field .control-select[multiple],.form__field .control-textarea{width:100%;height:calc(6*1.2em + 14px)}.form__field .control-value{display:inline-block;padding:6px 10px}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:active,.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:active,.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field .control-select{padding:0}.form__field .control-select option{box-sizing:border-box;padding:4px 10px;display:block}.form__field .control-select optgroup{font-weight:600;display:block;padding:4px 10px;line-height:33px;list-style:inside;font-style:normal}.form__field .control-range>.form__field:nth-child(2):before{content:"\2014";content:":";display:inline-block;margin-left:-25px;float:left;width:20px;line-height:33px;text-align:center}.form__field.choice{position:relative;z-index:1;padding-top:8px;padding-left:26px;padding-right:0}.form__field.choice .label{font-weight:500;padding:0;display:inline;float:none;line-height:18px}.form__field.choice input{position:absolute;top:8px;margin-top:3px!important}.form__field.choice input[disabled]+.label{opacity:.5;cursor:not-allowed}.control>.form__field.choice{max-width:380px}.control>.form__field.choice:nth-child(1):nth-last-child(2),.control>.form__field.choice:nth-child(2):nth-last-child(1){display:inline-block}.control>.form__field.choice:nth-child(1):nth-last-child(2)+.choice,.control>.form__field.choice:nth-child(2):nth-last-child(1)+.choice{margin-left:41px;margin-top:0}.control>.form__field.choice:nth-child(1):nth-last-child(2)+.choice:before,.control>.form__field.choice:nth-child(2):nth-last-child(1)+.choice:before{content:"";position:absolute;display:inline-block;height:20px;top:8px;left:-20px;width:1px;background:#ccc}.form__field.choice .label{cursor:pointer}.form__field.choice .label:before{content:"";position:absolute;z-index:1;border:1px solid #adadad;width:14px;height:14px;top:10px;left:0;border-radius:2px;background:url("../Magento_Ui/images/choice_bkg.png") no-repeat -100% -100%}.form__field.choice input:focus+.label:before{outline:0;border-color:#007bdb}.form__field.choice .control-radio+.label:before{border-radius:8px}.form__field.choice .control-radio:checked+.label:before{background-position:-26px -1px}.form__field.choice .control-checkbox:checked+.label:before{background-position:-1px -1px}.form__field.choice input{opacity:0}.fieldset>.form__field.choice{margin-left:30%}.form__field .control-after,.form__field .control-before{border:0;color:#858585;font-weight:300;font-size:15px;line-height:33px;display:inline-block;height:33px;box-sizing:border-box;padding:0 3px}.no-flexbox.no-flexboxlegacy .form__field .control-before,.no-flexbox.no-flexboxlegacy .form__field .control-addon{float:left;white-space:nowrap}.form__field .control-addon{display:inline-flex;max-width:380px;width:100%;flex-flow:row nowrap;position:relative;z-index:1}.form__field .control-addon>*{position:relative;z-index:1}.form__field .control-addon .control-text[disabled][type],.form__field .control-addon .control-select[disabled][type],.form__field .control-addon .control-select,.form__field .control-addon .control-text{background:transparent!important;border:0;width:auto;vertical-align:top;order:1;flex:1}.form__field .control-addon .control-text[disabled][type]:focus,.form__field .control-addon .control-select[disabled][type]:focus,.form__field .control-addon .control-select:focus,.form__field .control-addon .control-text:focus{box-shadow:none}.form__field .control-addon .control-text[disabled][type]:focus+label:before,.form__field .control-addon .control-select[disabled][type]:focus+label:before,.form__field .control-addon .control-select:focus+label:before,.form__field .control-addon .control-text:focus+label:before{outline:0;border-color:#007bdb}.form__field .control-addon .control-text[disabled][type]+label,.form__field .control-addon .control-select[disabled][type]+label,.form__field .control-addon .control-select+label,.form__field .control-addon .control-text+label{padding-left:10px;position:static!important;z-index:0}.form__field .control-addon .control-text[disabled][type]+label>*,.form__field .control-addon .control-select[disabled][type]+label>*,.form__field .control-addon .control-select+label>*,.form__field .control-addon .control-text+label>*{vertical-align:top;position:relative;z-index:2}.form__field .control-addon .control-text[disabled][type]+label:before,.form__field .control-addon .control-select[disabled][type]+label:before,.form__field .control-addon .control-select+label:before,.form__field .control-addon .control-text+label:before{box-sizing:border-box;border-radius:1px;border:1px solid #adadad;content:"";display:block;position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;background:#fff}.form__field .control-addon .control-text[disabled][type][disabled]+label:before,.form__field .control-addon .control-select[disabled][type][disabled]+label:before,.form__field .control-addon .control-select[disabled]+label:before,.form__field .control-addon .control-text[disabled]+label:before{opacity:.5;background:#e9e9e9}.form__field .control-after{order:3}.form__field .control-after:last-child{padding-right:10px}.form__field .control-before{order:0}.form__field .control-some{display:flex}.form__field [class*=control-grouped]{display:table;width:100%;table-layout:fixed;box-sizing:border-box}.form__field [class*=control-grouped]>.form__field{display:table-cell;width:50%;vertical-align:top}.form__field [class*=control-grouped]>.form__field>.control{width:100%;float:none}.form__field [class*=control-grouped]>.form__field:nth-child(n+2){padding-left:20px}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:active,.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:active,.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field [required]{box-shadow:none}fieldset.form__field{position:relative}fieldset.form__field [class*=control-grouped]>.form__field:first-child>.label,fieldset.form__field .control-fields>.form__field:first-child>.label{position:absolute;left:0;top:0;opacity:0;cursor:pointer;width:30%}.control-text+.ui-datepicker-trigger{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;line-height:inherit;font-weight:400;text-decoration:none;margin-left:-40px;display:inline-block}.control-text+.ui-datepicker-trigger img{display:none}.control-text+.ui-datepicker-trigger:focus,.control-text+.ui-datepicker-trigger:active{background:0 0;border:none}.control-text+.ui-datepicker-trigger:hover{background:0 0;border:none}.control-text+.ui-datepicker-trigger.disabled,.control-text+.ui-datepicker-trigger[disabled],fieldset[disabled] .control-text+.ui-datepicker-trigger{cursor:not-allowed;pointer-events:none;opacity:.5}.control-text+.ui-datepicker-trigger>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.control-text+.ui-datepicker-trigger>span.focusable:active,.control-text+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text+.ui-datepicker-trigger>span.focusable:active,.control-text+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text+.ui-datepicker-trigger:after{font-family:'icons-blank-theme';content:'\e612';font-size:38px;line-height:33px;color:#514943;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}[class*=tab-nav-item]:not(ul):active,[class*=tab-nav-item]:not(ul):focus{box-shadow:none;outline:none}.customer-index-edit .col-2-left-layout,.customer-index-edit .col-1-layout{background:#fff}.customer-index-edit{background:#fff}.customer-index-edit .col-2-left-layout{background:#fff}.customer-index-edit .main-col{padding-left:40px}.customer-index-edit .page-main-actions{background:0 0}.tab-nav.block{margin-bottom:40px}.tab-nav.block:first-child{margin-top:16px}.tab-nav.block .block-title{padding:7px 20px}.tab-nav-items{padding:0;border:1px solid #d3d3d3;box-shadow:0 0 4px rgba(50,50,50,.35);margin:0 0 40px;background:#f7f7f7}.tab-nav-item{padding:0;list-style-type:none;border-bottom:1px solid #e0e0e0;position:relative;margin:0 15px;z-index:1}.tab-nav-item:last-child{border-bottom:0}.tab-nav-item.ui-state-active{z-index:2;background:#fff;padding:1px 14px;border:2px solid #eb5202;margin:-1px}.tab-nav-item.ui-state-active .tab-nav-item-link{padding:13px 15px 13px;color:#eb5202}.tab-nav-item.ui-tabs-loading{position:relative;z-index:1}.tab-nav-item.ui-tabs-loading:before{content:"";display:block;position:absolute;z-index:2;background:url("../images/loader-2.gif") no-repeat 50% 50%;background-size:120px;width:20px;height:20px;top:13px;left:-10px}.tab-nav-item.ui-tabs-loading.ui-state-active:before{top:12px;left:4px}.tab-nav-item-link{display:block;padding:15px;color:#666;line-height:1}.tab-nav-item-link:focus,.tab-nav-item-link:active,.tab-nav-item-link:hover{outline:0;color:#eb5202;text-decoration:none}.ui-state-active .tab-nav-item-link{color:#666;font-weight:600}.tab-nav-item-link.changed{font-style:italic}.listing-tiles{overflow:hidden;margin-top:-10px;margin-left:-10px}.listing-tiles .listing-tile{background-color:#f2ebde;display:block;width:238px;height:200px;float:left;border:1px solid #676056;margin-top:10px;margin-left:10px;border-radius:4px;text-align:center}.listing-tiles .listing-tile.disabled{border-color:red}.listing-tiles .listing-tile.enabled{border-color:green}.listing .disabled{color:red}.listing .enabled{color:green}.pager{text-align:left;padding-bottom:10px}.pager:before,.pager:after{content:"";display:table}.pager:after{clear:both}.pager:before,.pager:after{content:"";display:table}.pager:after{clear:both}.pager [data-part=left]{display:inline-block;width:45%;float:left;text-align:left}.pager [data-part=right]{display:inline-block;width:45%;text-align:right;float:right;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.pager .action-next{cursor:pointer}.pager .action-previous{cursor:pointer}.pager{text-align:left}.pager [data-part=left]{display:inline-block;width:45%;text-align:left}.pager [data-part=right]{display:inline-block;width:45%;text-align:right;float:right}.grid .col-title{min-width:90px;text-align:center}.grid-actions [data-part=search]{display:inline-block;margin:0 30px}.grid-actions [data-part=search] input[type=text]{vertical-align:bottom;width:460px}.grid .actions-split .dropdown-menu{right:auto;left:auto;text-align:left;color:#676056;font-weight:400}.grid .actions-split .dropdown-menu:after{right:auto;left:9px}.grid .actions-split .dropdown-menu:before{right:auto;left:10px}.grid .grid-actions{padding:10px 0}.grid .hor-scroll{padding-top:10px}.grid .select-box{display:inline-block;vertical-align:top;margin:-12px -10px -7px;padding:12px 10px 7px;width:100%}.filters-toggle{background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:3px;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.filters-toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:30px;line-height:15px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-toggle:hover:after{color:inherit}.filters-toggle:active:after{color:inherit}.filters-toggle:focus,.filters-toggle:active{background:#cac3b4;border:1px solid #989287}.filters-toggle:hover{background:#cac3b4}.filters-toggle.disabled,.filters-toggle[disabled],fieldset[disabled] .filters-toggle{cursor:default;pointer-events:none;opacity:.5}.filters-toggle:focus,.filters-toggle:active{background:0 0;border:none}.filters-toggle:hover{background:0 0;border:none}.filters-toggle.disabled,.filters-toggle[disabled],fieldset[disabled] .filters-toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.filters-toggle:after{margin-top:2px;margin-left:-3px}.filters-toggle.active:after{content:'\e618'}.filters-current{padding:10px 0;display:none}.filters-current.active{display:block}.filters-items{margin:0;padding:0;list-style:none none;display:inline}.filters-item{display:inline-block;margin:0 5px 5px 0;padding:2px 2px 2px 4px;border-radius:3px;background:#f7f3eb}.filters-item .item-label{font-weight:600}.filters-item .item-label:after{content:": "}.filters-item .action-remove{background-image:none;background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:3px;vertical-align:middle;display:inline-block;text-decoration:none;padding:0}.filters-item .action-remove>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters-item .action-remove>span.focusable:active,.filters-item .action-remove>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-item .action-remove>span.focusable:active,.filters-item .action-remove>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-item .action-remove:before{font-family:'icons-blank-theme';content:'\e616';font-size:16px;line-height:16px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-item .action-remove:hover:before{color:inherit}.filters-item .action-remove:active:before{color:inherit}.filters-item .action-remove:focus,.filters-item .action-remove:active{background:#cac3b4;border:1px solid #989287}.filters-item .action-remove:hover{background:#cac3b4}.filters-item .action-remove.disabled,.filters-item .action-remove[disabled],fieldset[disabled] .filters-item .action-remove{cursor:default;pointer-events:none;opacity:.5}.filters-form{position:relative;z-index:1;margin:14px 0;background:#fff;border:1px solid #bbb;box-shadow:0 3px 3px rgba(0,0,0,.15)}.filters-form .action-close{position:absolute;top:3px;right:7px;background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:3px;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.filters-form .action-close>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters-form .action-close>span.focusable:active,.filters-form .action-close>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-form .action-close>span.focusable:active,.filters-form .action-close>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-form .action-close:before{font-family:'icons-blank-theme';content:'\e616';font-size:42px;line-height:42px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-form .action-close:hover:before{color:inherit}.filters-form .action-close:active:before{color:inherit}.filters-form .action-close:focus,.filters-form .action-close:active{background:#cac3b4;border:1px solid #989287}.filters-form .action-close:hover{background:#cac3b4}.filters-form .action-close.disabled,.filters-form .action-close[disabled],fieldset[disabled] .filters-form .action-close{cursor:default;pointer-events:none;opacity:.5}.filters-form .action-close:focus,.filters-form .action-close:active{background:0 0;border:none}.filters-form .action-close:hover{background:0 0;border:none}.filters-form .action-close.disabled,.filters-form .action-close[disabled],fieldset[disabled] .filters-form .action-close{cursor:not-allowed;pointer-events:none;opacity:.5}.filters-actions{margin:18px;text-align:right}.filters-fieldset{padding-bottom:0}.filters-fieldset .field{border:0;margin:0 0 20px;box-sizing:border-box;display:inline-block;padding:0 12px 0 0;width:33.33333333%;vertical-align:top}.filters-fieldset .field:before,.filters-fieldset .field:after{content:"";display:table}.filters-fieldset .field:after{clear:both}.filters-fieldset .field:before,.filters-fieldset .field:after{content:"";display:table}.filters-fieldset .field:after{clear:both}.filters-fieldset .field.choice:before,.filters-fieldset .field.no-label:before{box-sizing:border-box;content:" ";height:1px;float:left;padding:6px 15px 0 0;width:35%}.filters-fieldset .field .description{box-sizing:border-box;float:left;padding:6px 15px 0 0;text-align:right;width:35%}.filters-fieldset .field:not(.choice)>.label{box-sizing:border-box;float:left;padding:6px 15px 0 0;text-align:right;width:35%}.filters-fieldset .field:not(.choice)>.control{float:left;width:65%}.filters-fieldset .field:last-child{margin-bottom:0}.filters-fieldset .field+.fieldset{clear:both}.filters-fieldset .field>.label{font-weight:700}.filters-fieldset .field>.label+br{display:none}.filters-fieldset .field .choice input{vertical-align:top}.filters-fieldset .field .fields.group:before,.filters-fieldset .field .fields.group:after{content:"";display:table}.filters-fieldset .field .fields.group:after{clear:both}.filters-fieldset .field .fields.group:before,.filters-fieldset .field .fields.group:after{content:"";display:table}.filters-fieldset .field .fields.group:after{clear:both}.filters-fieldset .field .fields.group .field{box-sizing:border-box;float:left}.filters-fieldset .field .fields.group.group-2 .field{width:50% !important}.filters-fieldset .field .fields.group.group-3 .field{width:33.3% !important}.filters-fieldset .field .fields.group.group-4 .field{width:25% !important}.filters-fieldset .field .fields.group.group-5 .field{width:20% !important}.filters-fieldset .field .addon{display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;padding:0;width:100%}.filters-fieldset .field .addon textarea,.filters-fieldset .field .addon select,.filters-fieldset .field .addon input{-ms-flex-order:2;-webkit-order:2;order:2;-webkit-flex-basis:100%;flex-basis:100%;box-shadow:none;display:inline-block;margin:0;width:auto}.filters-fieldset .field .addon .addbefore,.filters-fieldset .field .addon .addafter{-ms-flex-order:3;-webkit-order:3;order:3;display:inline-block;box-sizing:border-box;background:#fff;border:1px solid #c2c2c2;border-radius:1px;height:32px;width:100%;padding:0 9px;font-size:14px;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.428571429;background-clip:padding-box;vertical-align:baseline;width:auto;white-space:nowrap;vertical-align:middle}.filters-fieldset .field .addon .addbefore:disabled,.filters-fieldset .field .addon .addafter:disabled{opacity:.5}.filters-fieldset .field .addon .addbefore::-moz-placeholder,.filters-fieldset .field .addon .addafter::-moz-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore::-webkit-input-placeholder,.filters-fieldset .field .addon .addafter::-webkit-input-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore:-ms-input-placeholder,.filters-fieldset .field .addon .addafter:-ms-input-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore{float:left;-ms-flex-order:1;-webkit-order:1;order:1}.filters-fieldset .field .additional{margin-top:10px}.filters-fieldset .field.required>.label:after{content:'*';font-size:1.2rem;color:#e02b27;margin:0 0 0 5px}.filters-fieldset .field .note{font-size:1.2rem;margin:3px 0 0;padding:0;display:inline-block;text-decoration:none}.filters-fieldset .field .note:before{font-family:'icons-blank-theme';content:'\e618';font-size:24px;line-height:12px;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filters-fieldset .field .label{color:#676056;font-size:13px;font-weight:600;margin:0}.filters .field-date .group .hasDatepicker{width:100%;padding-right:30px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;line-height:inherit;font-weight:400;text-decoration:none;margin-left:-33px;display:inline-block;width:30px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger img{display:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:focus,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:active{background:0 0;border:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:hover{background:0 0;border:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger.disabled,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger[disabled],fieldset[disabled] .filters .field-date .group .hasDatepicker+.ui-datepicker-trigger{cursor:not-allowed;pointer-events:none;opacity:.5}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:active,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:active,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:after{font-family:'icons-blank-theme';content:'\e612';font-size:35px;line-height:30px;color:#514943;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filters .field-range .group .field{margin-bottom:0}.filters .field-range .group .control{width:100%;box-sizing:border-box;padding-right:0;position:relative;z-index:1}.mass-select{position:relative;margin:-6px -10px;padding:6px 2px 6px 10px;z-index:1;white-space:nowrap}.mass-select.active{background:rgba(0,0,0,.2)}.mass-select-toggle{background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:3px;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.mass-select-toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.mass-select-toggle>span.focusable:active,.mass-select-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.mass-select-toggle>span.focusable:active,.mass-select-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.mass-select-toggle:before{font-family:'icons-blank-theme';content:'\e607';font-size:30px;line-height:15px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.mass-select-toggle:hover:before{color:inherit}.mass-select-toggle:active:before{color:inherit}.mass-select-toggle:focus,.mass-select-toggle:active{background:#cac3b4;border:1px solid #989287}.mass-select-toggle:hover{background:#cac3b4}.mass-select-toggle.disabled,.mass-select-toggle[disabled],fieldset[disabled] .mass-select-toggle{cursor:default;pointer-events:none;opacity:.5}.mass-select-toggle:focus,.mass-select-toggle:active{background:0 0;border:none}.mass-select-toggle:hover{background:0 0;border:none}.mass-select-toggle.disabled,.mass-select-toggle[disabled],fieldset[disabled] .mass-select-toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.mass-select-toggle:before{margin-top:-2px;text-indent:-5px;color:#fff}.mass-select-toggle:hover:before{color:#fff}.mass-select-toggle:active:before,.mass-select-toggle.active:before{content:'\e618'}.mass-select-field{display:inline}.mass-select-menu{display:none;position:absolute;top:100%;left:0;text-align:left;margin:0;padding:0;list-style:none none;background:#fff;border:1px solid #bbb;min-width:175px;box-shadow:0 3px 3px rgba(0,0,0,.15)}.mass-select-menu li{margin:0;padding:4px 15px;border-bottom:1px solid #e5e5e5}.mass-select-menu li:hover{background:#e8e8e8;cursor:pointer}.mass-select-menu span{font-weight:400;font-size:13px;color:#645d53}.mass-select-menu.active{display:block}.grid-loading-mask{position:absolute;left:0;top:0;right:0;bottom:0;background:rgba(255,255,255,.5);z-index:100}.grid-loading-mask .grid-loader{position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;width:218px;height:149px;background:url('../images/loader-2.gif') 50% 50% no-repeat}.addon input{border-width:1px 0 1px 1px}.addon input~.addafter strong{display:inline-block;background:#fff;line-height:24px;margin:0 3px 0 -2px;padding-left:4px;padding-right:4px;position:relative;font-size:12px;top:0}.addon input:focus~.addafter{border-color:#75b9f0;box-shadow:0 0 8px rgba(82,168,236,.6)}.addon input:focus~.addafter strong{margin-top:0}.addon .addafter{background:0 0;color:#a6a6a6;border-width:1px 1px 1px 0;border-radius:2px 2px 0 0;padding:0;border-color:#ada89e}.addon .pager input{border-width:1px}.field .control input[type=text][disabled],.field .control input[type=text][disabled]~.addafter,.field .control select[disabled],.field .control select[disabled]~.addafter{background-color:#fff;border-color:#eee;box-shadow:none;color:#999}.field .control input[type=text][disabled]~.addafter strong,.field .control select[disabled]~.addafter strong{background-color:#fff}.field-price.addon{direction:rtl}.field-price.addon>*{direction:ltr}.field-price.addon .addafter{border-width:1px 0 1px 1px;border-radius:2px 0 0 2px}.field-price.addon input:first-child{border-radius:0 2px 2px 0}.field-price input{border-width:1px 1px 1px 0}.field-price input:focus{box-shadow:0 0 8px rgba(82,168,236,.6)}.field-price input:focus~label.addafter{box-shadow:0 0 8px rgba(82,168,236,.6)}.field-price input~label.addafter strong{margin-left:2px;margin-right:-2px}.field-price.addon>input{width:99px;float:left}.field-price .control{position:relative}.field-price label.mage-error{position:absolute;left:0;top:30px}.version-fieldset .grid-actions{border-bottom:1px solid #f2ebde;margin:0 0 15px;padding:0 0 15px}.navigation>ul,.message-system,.page-header,.page-actions.fixed .page-actions-inner,.page-content,.page-footer{width:auto;min-width:960px;max-width:1300px;margin:0 auto;padding-left:15px;padding-right:15px;box-sizing:border-box;width:100%}.pager label.page,.filters .field-range .group .label,.mass-select-field .label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visually-hidden.focusable:active,.visually-hidden.focusable:focus,.pager label.page.focusable:active,.pager label.page.focusable:focus,.filters .field-range .group .label.focusable:active,.filters .field-range .group .label.focusable:focus,.mass-select-field .label.focusable:active,.mass-select-field .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}table th.required:after,.data-table th.required-entry:after,.data-table td.required-entry:after,.grid-actions .filter.required .label span:after,.grid-actions .required:after,.accordion .config .data-table td.required-entry:after{content:'*';color:#e22626;font-weight:400;margin-left:3px}.grid th.required:after,.grid th .required:after{content:'*';color:#f9d4d4;font-weight:400;margin-left:3px}.grid td.col-period,.grid td.col-date,.grid td.col-date_to,.grid td.col-date_from,.grid td.col-ended_at,.grid td.col-created_at,.grid td.col-updated_at,.grid td.col-customer_since,.grid td.col-session_start_time,.grid td.col-last_activity,.grid td.col-email,.grid td.col-name,.grid td.col-sku,.grid td.col-firstname,.grid td.col-lastname,.grid td.col-title,.grid td.col-label,.grid td.col-product,.grid td.col-set_name,.grid td.col-websites,.grid td.col-time,.grid td.col-billing_name,.grid td.col-shipping_name,.grid td.col-phone,.grid td.col-type,.product-options .grouped-items-table .col-name,.product-options .grouped-items-table .col-sku,.sales-order-create-index .data-table .col-product,[class^=' adminhtml-rma-'] .fieldset-wrapper .data-table td,[class^=' adminhtml-rma-'] .grid .col-product_sku,[class^=' adminhtml-rma-'] .grid .col-product_name,.col-grid_segment_name,.adminhtml-catalog-event-index .col-category,[class^=' catalog-search'] .col-search_query,[class^=' catalog-search'] .col-synonym_for,[class^=' catalog-search'] .col-redirect,.adminhtml-urlrewrite-index .col-request_path,.adminhtml-cms-page-index .col-title,.adminhtml-cms-page-index .col-identifier,.adminhtml-cms-hierarchy-index .col-title,.adminhtml-cms-hierarchy-index .col-identifier,.col-banner_name,.adminhtml-widget-instance-index .col-title,.reports-index-search .col-query_text,.adminhtml-rma-item-attribute-index .grid .col-attr-code,.adminhtml-system-store-index .grid td,.catalog-product-attribute-index .col-attr-code,.catalog-product-attribute-index .col-label,.adminhtml-export-index .col-code,.adminhtml-logging-index .grid .col-fullaction,.adminhtml-system-variable-index .grid .col-code,.adminhtml-logging-index .grid .col-info,.dashboard-secondary .dashboard-item tr>td:first-child,.ui-tabs-panel .dashboard-data .col-name,.data-table-td-max .data-table td,[class^=' adminhtml-rma-'] .fieldset-wrapper .accordion .config .data-table td,.data-table-td-max .accordion .config .data-table td,.order-account-information .data-table td,[class^=' adminhtml-rma-'] .rma-request-details .data-table td{overflow:hidden;text-overflow:ellipsis}td.col-period,td.col-date,td.col-date_to,td.col-date_from,td.col-ended_at,td.col-created_at,td.col-updated_at,td.col-customer_since,td.col-session_start_time,td.col-time,td.col-sku,td.col-type,[class^=' adminhtml-rma-'] #rma_items_grid_table .headings th,.adminhtml-process-list .col-action a,.adminhtml-process-list .col-mode{white-space:nowrap}table thead tr th:first-child,table tfoot tr th:first-child,table tfoot tr td:first-child{border-left:0}table thead tr th:last-child,table tfoot tr th:last-child,table tfoot tr td:last-child{border-right:0}.form-inline .grid-actions .label,.form-inline .massaction .label{padding:0;width:auto}.grid .col-action,.grid .col-actions,.grid .col-qty,.grid .col-purchases,.catalog-product-edit .ui-tabs-panel .grid .col-price,.catalog-product-edit .ui-tabs-panel .grid .col-position{width:50px}.grid .col-order-number,.grid .col-real_order_id,.grid .col-invoice-number,.grid .col-increment_id,.grid .col-transaction-id,.grid .col-parent-transaction-id,.grid .col-reference_id,.grid .col-status,.grid .col-price,.grid .col-position,.grid .col-base_grand_total,.grid .col-grand_total,.grid .col-sort_order,.grid .col-carts,.grid .col-priority,.grid .col-severity,.sales-order-create-index .col-in_products,[class^=' reports-'] [class^=col-total],[class^=' reports-'] [class^=col-average],[class^=' reports-'] [class^=col-ref-],[class^=' reports-'] [class^=col-rate],[class^=' reports-'] [class^=col-tax-amount],[class^=' adminhtml-customer-'] .col-required,.adminhtml-rma-item-attribute-index .col-required,[class^=' adminhtml-customer-'] .col-system,.adminhtml-rma-item-attribute-index .col-system,[class^=' adminhtml-customer-'] .col-is_visible,.adminhtml-rma-item-attribute-index .col-is_visible,[class^=' adminhtml-customer-'] .col-sort_order,.adminhtml-rma-item-attribute-index .col-sort_order,.catalog-product-attribute-index [class^=' col-is_'],.catalog-product-attribute-index .col-required,.catalog-product-attribute-index .col-system,.adminhtml-test-index .col-is_listed,[class^=' tests-report-test'] [class^=col-inv-]{width:70px}.grid .col-phone,.sales-order-view .grid .col-period,.sales-order-create-index .col-phone,[class^=' adminhtml-rma-'] .grid .col-product_sku,.adminhtml-rma-edit .col-product,.adminhtml-rma-edit .col-sku,.catalog-product-edit .ui-tabs-panel .grid .col-name,.catalog-product-edit .ui-tabs-panel .grid .col-type,.catalog-product-edit .ui-tabs-panel .grid .col-sku,.customer-index-index .grid .col-customer_since,.customer-index-index .grid .col-billing_country_id,[class^=' customer-index-'] .fieldset-wrapper .grid .col-created_at,[class^=' customer-index-'] .accordion .grid .col-created_at{max-width:70px;width:70px}.sales-order-view .grid .col-name,.sales-order-create-index .data-table .col-product,[class^=' adminhtml-rma-'] .grid .col-name,[class^=' adminhtml-rma-'] .grid .col-product,[class^=' catalog-search'] .col-search_query,[class^=' catalog-search'] .col-synonym_for,[class^=' catalog-search'] .col-redirect,.adminhtml-urlrewrite-index .col-request_path,.reports-report-shopcart-abandoned .grid .col-name,.tax-rule-index .grid .col-title,.adminhtml-rma-item-attribute-index .grid .col-attr-code,.dashboard-secondary .dashboard-item tr>td:first-child{max-width:150px;width:150px}[class^=' sales-order-'] .grid .col-name,.catalog-category-edit .grid .col-name,.adminhtml-catalog-event-index .col-category,.adminhtml-banner-edit .grid .col-name,.reports-report-product-lowstock .grid .col-sku,.newsletter-problem-index .grid .col-name,.newsletter-problem-index .grid .col-subject,.newsletter-problem-index .grid .col-product,.adminhtml-rma-item-attribute-index .grid .col-label,.adminhtml-export-index .col-label,.adminhtml-export-index .col-code,.adminhtml-scheduled-operation-index .grid .col-name,.adminhtml-logging-index .grid .col-fullaction,.test-report-customer-wishlist-wishlist .grid .col-name,.test-report-customer-wishlist-wishlist .grid .col-subject,.test-report-customer-wishlist-wishlist .grid .col-product{max-width:220px;width:220px}.grid .col-period,.grid .col-date,.grid .col-date_to,.grid .col-date_from,.grid .col-ended_at,.grid .col-created_at,.grid .col-updated_at,.grid .col-customer_since,.grid .col-session_start_time,.grid .col-last_activity,.grid .col-email,.grid .col-items_total,.grid .col-firstname,.grid .col-lastname,.grid .col-status-default,.grid .col-websites,.grid .col-time,.grid .col-billing_name,.grid .col-shipping_name,.sales-order-index .grid .col-name,.product-options .grouped-items-table .col-name,.product-options .grouped-items-table .col-sku,[class^=' sales-order-view'] .grid .col-customer_name,[class^=' adminhtml-rma-'] .grid .col-product_name,.catalog-product-index .grid .col-name,.catalog-product-review-index .grid .col-name,.catalog-product-review-index .grid .col-title,.customer-index-edit .ui-tabs-panel .grid .col-name,.review-product-index .grid .col-name,.adminhtml-cms-page-index .col-title,.adminhtml-cms-page-index .col-identifier,.catalog-product-attribute-index .col-attr-code,.catalog-product-attribute-index .col-label,.adminhtml-logging-index .grid .col-info{max-width:110px;width:110px}.grid .col-name,.grid .col-product,.col-banner_name,.adminhtml-widget-instance-index .col-title,[class^=' adminhtml-customer-'] .col-label,.adminhtml-rma-item-attribute-index .col-label,.adminhtml-system-variable-index .grid .col-code,.ui-tabs-panel .dashboard-data .col-name,.adminhtml-test-index .col-label{max-width:370px;width:370px}.col-grid_segment_name,.reports-index-search .col-query_text{max-width:570px;width:570px}[class^=' adminhtml-rma-'] .fieldset-wrapper .data-table td,.reports-report-product-lowstock .grid .col-name,.reports-report-shopcart-product .grid .col-name,.reports-report-review-customer .grid .col-name,[class^=' adminhtml-rma-'] .fieldset-wrapper .accordion .config .data-table td{max-width:670px;width:670px}.reports-report-sales-invoiced .grid .col-period,.reports-report-sales-refunde .grid .col-period,[class^=' tests-report-test'] .grid .col-period{width:auto}.grid .col-select,.grid .col-id,.grid .col-number{width:40px}.sales-order-create-index .grid,.sales-order-create-index .grid-actions,.adminhtml-export-index .grid-actions,.adminhtml-export-index .grid{padding-left:0;padding-right:0}[class^=' adminhtml-rma-'] .col-actions a,[class^=' customer-index-'] .col-action a,.adminhtml-notification-index .col-actions a{display:block;margin:0 0 3px;white-space:nowrap}.data-table-td-max .accordion .config .data-table td,.order-account-information .data-table td,[class^=' adminhtml-rma-'] .rma-request-details .data-table td{max-width:250px;width:250px}.catalog-product-edit .ui-tabs-panel .grid .hor-scroll,.catalog-product-index .grid .hor-scroll,.review-product-index .grid .hor-scroll,.adminhtml-rma-edit .hor-scroll{overflow-x:auto}.add-clearer:after,.massaction:after,.navigation>ul:after{content:"";display:table;clear:both}.test-content{width:calc(20px + 100*0.2)}.test-content:before{content:'.test {\A ' attr(data-attribute) ': 0.2em;' '\A content:\'';white-space:pre}.test-content:after{content:' Test\';\A}' "\A" '\A.test + .test._other ~ ul > li' " {\A height: @var;\A content: ' + ';\A}";white-space:pre}.test-content-calc{width:calc((100%/12*2) - 10px)}.test-svg-xml-image{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 38 40"><style>.st0{fill:none;stroke:%23ffffff;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}</style><circle cx="14.7" cy="14.7" r="13.7" class="st0"/><path d="M23.9 24.7L37 39" class="st0"/></svg>') no-repeat left center} \ No newline at end of file +table>caption{margin-bottom:5px}table thead{background:#676056;color:#f7f3eb}table thead .headings{background:#807a6e}table thead a{color:#f7f3eb;display:block}table thead a label{color:#f7f3eb;cursor:pointer;display:block}table thead a:hover,table thead a:focus{color:#dac7a2;text-decoration:none}table tfoot{background:#f2ebde;color:#676056}table tfoot tr th,table tfoot tr td{text-align:left}table th{background:0 0;border:solid #cac3b4;border-width:0 1px;font-size:14px;padding:6px 10px;text-align:center}table td{border:solid #cac3b4;border-width:0 1px;padding:6px 10px 7px;vertical-align:top}table tbody tr td{background:#fff;color:#676056;padding-top:12px}table tbody tr td:first-child{border-left:0}table tbody tr td:first-child input[type=checkbox]{margin:0}table tbody tr td:last-child{border-right:0}table tbody tr:last-child th,table tbody tr:last-child td{border-bottom-width:1px}table tbody tr:nth-child(odd) td,table tbody tr:nth-child(odd) th{background-color:#f7f3eb}table tbody.even tr td{background:#fff}table tbody.odd tr td{background:#f7f3eb}table .dropdown-menu li{padding:7px 15px;line-height:14px;cursor:pointer}table .col-draggable .draggable-handle{float:left;position:relative;top:0}.not-sort{padding-right:10px}.sort-arrow-asc,.sort-arrow-desc{padding-right:10px;position:relative}.sort-arrow-asc:after,.sort-arrow-desc:after{right:-11px;top:-1px;position:absolute;width:23px}.sort-arrow-asc:hover:after,.sort-arrow-desc:hover:after{color:#dac7a2}.sort-arrow-asc{display:inline-block;text-decoration:none}.sort-arrow-asc:after{font-family:'icons-blank-theme';content:'\e626';font-size:13px;line-height:inherit;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.sort-arrow-asc:hover:after{color:#dac7a2}.sort-arrow-desc{display:inline-block;text-decoration:none}.sort-arrow-desc:after{font-family:'icons-blank-theme';content:'\e623';font-size:13px;line-height:inherit;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.sort-arrow-desc:hover:after{color:#dac7a2}.grid-actions .input-text,.pager .input-text,.massaction .input-text,.filter .input-text,.grid-actions select,.pager select,.massaction select,.filter select,.grid-actions .select,.pager .select,.massaction .select,.filter .select{border-color:#989287;box-shadow:none;border-radius:1px;height:28px;margin:0 10px 0 0}.filter th{border:0 solid #676056;padding:6px 3px;vertical-align:top}.filter .ui-datepicker-trigger{cursor:pointer;margin-top:2px}.filter .input-text{padding:0 5px}.filter .range-line:not(:last-child){margin-bottom:5px}.filter .date{padding-right:28px;position:relative;display:inline-block;text-decoration:none}.filter .date .hasDatepicker{vertical-align:top;width:99%}.filter .date img{cursor:pointer;height:25px;width:25px;right:0;position:absolute;vertical-align:middle;z-index:2;opacity:0}.filter .date:before{font-family:'icons-blank-theme';content:'\e612';font-size:42px;line-height:30px;color:#f7f3eb;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filter .date:hover:before{color:#dac7a2}.filter .date:before{height:29px;margin-left:5px;position:absolute;right:-3px;top:-3px;width:35px}.filter select{border-color:#cac3b4;margin:0;padding:0;width:99%}.filter input.input-text{border-color:#cac3b4;margin:0;width:99%}.filter input.input-text::-webkit-input-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text::-moz-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text:-moz-placeholder{color:#989287 !important;text-transform:lowercase}.filter input.input-text:-ms-input-placeholder{color:#989287 !important;text-transform:lowercase}.grid{background:#fff;color:#676056;font-size:13px;font-weight:400;padding:15px}.grid table{width:100%}.grid tbody tr.selected th,.grid tbody tr.selected td,.grid tbody tr:hover th,.grid tbody tr:hover td,.grid tbody tr:nth-child(odd):hover th,.grid tbody tr:nth-child(odd):hover td{background-color:#f2ebde;cursor:pointer}.grid tbody tr.selected th.empty-text,.grid tbody tr.selected td.empty-text,.grid tbody tr:hover th.empty-text,.grid tbody tr:hover td.empty-text,.grid tbody tr:nth-child(odd):hover th.empty-text,.grid tbody tr:nth-child(odd):hover td.empty-text{background-color:#f7f3eb;cursor:default}.grid .empty-text{font:400 20px/1.2 'Open Sans',sans-serif;text-align:center;white-space:nowrap}.grid .col-sku{max-width:100px;width:100px}.grid .col-select,.grid .col-massaction{text-align:center}.grid .editable .input-text{width:65px}.grid .col-actions .action-select{background:#fff;border-color:#989287;height:28px;margin:0;padding:4px 4px 5px;width:80px}.grid .col-position.editable{white-space:nowrap}.grid .col-position.editable .input-text{margin:-7px 5px 0;width:70%}.eq-ie9 .hor-scroll{display:inline-block;min-height:0;overflow-y:hidden;overflow-x:auto;width:100%}.data-table{border-collapse:separate;width:100%}.data-table thead,.data-table tfoot,.data-table th,.accordion .config .data-table thead th,.accordion .config .data-table tfoot td,.accordion .config .accordion .config .data-table tfoot td th{background:#fff;color:#676056;font-size:13px;font-weight:600}.data-table th{text-align:left}.data-table thead th,.accordion .config .data-table thead th th,.accordion .config .data-table tfoot td th,.accordion .config .accordion .config .data-table tfoot td th th{border:solid #c9c2b8;border-width:0 0 1px;padding:7px}.data-table td,.data-table tbody tr th,.data-table tbody tr td,.accordion .config .data-table td{background:#fff;border-width:0;padding:5px 7px;vertical-align:middle}.data-table tbody tr:nth-child(odd) th,.data-table tbody tr:nth-child(odd) td,.accordion .config .data-table tbody tr:nth-child(odd) td{background:#fbfaf6}.data-table tbody.odd tr th,.data-table tbody.odd tr td{background:#fbfaf6}.data-table tbody.even tr th,.data-table tbody.even tr td{background:#fff}.data-table tfoot tr:last-child th,.data-table tfoot tr:last-child td,.data-table .accordion .config .data-table tfoot tr:last-child td{border:0}.data-table.order-tables tbody td{vertical-align:top}.data-table.order-tables tbody:hover tr th,.data-table.order-tables tbody:hover tr td{background:#f7f3eb}.data-table.order-tables tfoot td{background:#f2ebde;color:#676056;font-size:13px;font-weight:600}.data-table input[type=text]{width:98%;padding-left:1%;padding-right:1%}.data-table select{margin:0;box-sizing:border-box}.data-table .col-actions .actions-split{margin-top:4px}.data-table .col-actions .actions-split [class^=action-]{background:0 0;border:1px solid #c8c3b5;padding:3px 5px;color:#bbb3a6;font-size:12px}.data-table .col-actions .actions-split [class^=action-]:first-child{border-right:0}.data-table .col-actions .actions-split .dropdown-menu{margin-top:-1px}.data-table .col-actions .actions-split .dropdown-menu a{display:block;color:#333;text-decoration:none}.data-table .col-actions .actions-split.active .action-toggle{position:relative;border-bottom-right-radius:0;box-shadow:none;background:#fff}.data-table .col-actions .actions-split.active .action-toggle:after{position:absolute;top:100%;left:0;right:0;height:2px;margin-top:-1px;background:#fff;content:'';z-index:2}.data-table .col-actions .actions-split.active .action-toggle .dropdown-menu{border-top-right-radius:0}.data-table .col-default{white-space:nowrap;text-align:center;vertical-align:middle}.data-table .col-delete{text-align:center;width:32px}.data-table .col-file{white-space:nowrap}.data-table .col-file input,.data-table .col-file .input-text{margin:0 5px;width:40%}.data-table .col-file input:first-child,.data-table .col-file .input-text:first-child{margin-left:0}.data-table .col-actions-add{padding:10px 0}.grid-actions{background:#fff;font-size:13px;line-height:28px;padding:10px 15px;position:relative}.grid-actions+.grid{padding-top:5px}.grid-actions .export,.grid-actions .filter-actions{float:right;margin-left:10px;vertical-align:top}.grid-actions .import{display:block;vertical-align:top}.grid-actions .action-reset{background:0 0;border:0;display:inline;line-height:1.42857143;padding:0;color:#1979c3;text-decoration:none;margin:6px 10px 0 0;vertical-align:top}.grid-actions .action-reset:visited{color:purple;text-decoration:none}.grid-actions .action-reset:hover{color:#006bb4;text-decoration:underline}.grid-actions .action-reset:active{color:#ff5501;text-decoration:underline}.grid-actions .action-reset:hover{color:#006bb4}.grid-actions .action-reset:hover,.grid-actions .action-reset:active,.grid-actions .action-reset:focus{background:0 0;border:0}.grid-actions .action-reset.disabled,.grid-actions .action-reset[disabled],fieldset[disabled] .grid-actions .action-reset{color:#1979c3;text-decoration:underline;cursor:default;pointer-events:none;opacity:.5}.grid-actions .import .label,.grid-actions .export .label,.massaction>.entry-edit .label{margin:0 14px 0 0;vertical-align:inherit}.grid-actions .import .action-,.grid-actions .export .action-,.grid-actions .filter-actions .action-,.massaction>.entry-edit .action-{vertical-align:inherit}.grid-actions .filter .date{float:left;margin:0 15px 0 0;position:relative}.grid-actions .filter .date:before{color:#676056;top:1px}.grid-actions .filter .date:hover:before{color:#31302b}.grid-actions .filter .label{margin:0}.grid-actions .filter .hasDatepicker{margin:0 5px;width:80px}.grid-actions .filter .show-by .select{margin-left:5px;padding:4px 4px 5px;vertical-align:top;width:auto}.grid-actions .filter.required:after{content:''}.grid-actions img{vertical-align:middle;height:22px;width:22px}.grid-actions .validation-advice{background:#f9d4d4;border:1px solid #e22626;border-radius:3px;color:#e22626;margin:5px 0 0;padding:3px 7px;position:absolute;white-space:nowrap;z-index:5}.grid-actions .validation-advice:before{width:0;height:0;border:5px solid transparent;border-bottom-color:#e22626;content:'';left:50%;margin-left:-5px;position:absolute;top:-11px}.grid-actions input[type=text].validation-failed{border-color:#e22626;box-shadow:0 0 8px rgba(226,38,38,.6)}.grid-actions .link-feed{white-space:nowrap}.pager{font-size:13px}.grid .pager{margin:15px 0 0;position:relative;text-align:center}.pager .pages-total-found{margin-right:25px}.pager .view-pages .select{margin:0 5px}.pager .link-feed{font-size:12px;margin:7px 15px 0 0;position:absolute;right:0;top:0}.pager .action-previous,.pager .action-next{background:0 0;border:0;display:inline;margin:0;padding:0;color:#1979c3;text-decoration:none;line-height:.6;overflow:hidden;width:20px}.pager .action-previous:visited,.pager .action-next:visited{color:purple;text-decoration:none}.pager .action-previous:hover,.pager .action-next:hover{color:#006bb4;text-decoration:underline}.pager .action-previous:active,.pager .action-next:active{color:#ff5501;text-decoration:underline}.pager .action-previous:hover,.pager .action-next:hover{color:#006bb4}.pager .action-previous:hover,.pager .action-next:hover,.pager .action-previous:active,.pager .action-next:active,.pager .action-previous:focus,.pager .action-next:focus{background:0 0;border:0}.pager .action-previous.disabled,.pager .action-next.disabled,.pager .action-previous[disabled],.pager .action-next[disabled],fieldset[disabled] .pager .action-previous,fieldset[disabled] .pager .action-next{color:#1979c3;text-decoration:underline;cursor:default;pointer-events:none;opacity:.5}.pager .action-previous:before,.pager .action-next:before{margin-left:-10px}.pager .action-previous.disabled,.pager .action-next.disabled{opacity:.3}.pager .action-previous{display:inline-block;text-decoration:none}.pager .action-previous>span{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.pager .action-previous>span.focusable:active,.pager .action-previous>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-previous>span.focusable:active,.pager .action-previous>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-previous:before{font-family:'icons-blank-theme';content:'\e617';font-size:40px;line-height:inherit;color:#026294;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.pager .action-previous:hover:before{color:#007dbd}.pager .action-next{display:inline-block;text-decoration:none}.pager .action-next>span{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.pager .action-next>span.focusable:active,.pager .action-next>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-next>span.focusable:active,.pager .action-next>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.pager .action-next:before{font-family:'icons-blank-theme';content:'\e608';font-size:40px;line-height:inherit;color:#026294;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.pager .action-next:hover:before{color:#007dbd}.pager .input-text{height:25px;line-height:16px;margin-right:5px;text-align:center;width:25px;vertical-align:top}.pager .pages-total{line-height:25px;vertical-align:top}.massaction{background:#fff;border-top:1px solid #f2ebde;font-size:13px;line-height:28px;padding:15px 15px 0}.massaction>.entry-edit{float:right}.massaction>.entry-edit .field-row{display:inline-block;vertical-align:top}.massaction>.entry-edit .validation-advice{display:none !important}.massaction>.entry-edit .form-inline{display:inline-block}.massaction>.entry-edit .label{padding:0;width:auto}.massaction>.entry-edit .action-{vertical-align:top}.massaction .select.validation-failed{border:1px dashed #e22626;background:#f9d4d4}.grid-severity-critical,.grid-severity-major,.grid-severity-notice,.grid-severity-minor{background:#feeee1;border:1px solid #ed4f2e;color:#ed4f2e;display:block;padding:0 3px;font-weight:700;line-height:17px;text-transform:uppercase;text-align:center}.grid-severity-critical,.grid-severity-major{border-color:#e22626;background:#f9d4d4;color:#e22626}.grid-severity-notice{border-color:#5b8116;background:#d0e5a9;color:#185b00}.grid tbody td input[type=text],.data-table tbody td input[type=text],.grid tbody th input[type=text],.data-table tbody th input[type=text],.grid tbody td .input-text,.data-table tbody td .input-text,.grid tbody th .input-text,.data-table tbody th .input-text,.grid tbody td select,.data-table tbody td select,.grid tbody th select,.data-table tbody th select,.grid tbody td .select,.data-table tbody td .select,.grid tbody th .select,.data-table tbody th .select{width:99%}.ui-tabs-panel .grid .col-sku{max-width:150px;width:150px}.col-indexer_status,.col-indexer_mode{width:160px}.fieldset-wrapper .grid-actions+.grid{padding-top:15px}.fieldset-wrapper .grid-actions{padding:10px 0 0}.fieldset-wrapper .grid{padding:0}.fieldset-wrapper .massaction{padding:0;border-top:none;margin-bottom:15px}.accordion .grid{padding:0}.ui-dialog-content .grid-actions,.ui-dialog-content .grid{padding-left:0;padding-right:0}.qty-table td{border:0;padding:0 5px 3px}.sales-order-create-index .sales-order-create-index .grid table .action-configure{float:right}.sales-order-create-index .data-table .border td{padding-bottom:15px}.sales-order-create-index .actions.update{margin:10px 0}.adminhtml-order-shipment-new .grid .col-product{max-width:770px;width:770px}.customer-index-index .grid .col-name{max-width:90px;width:90px}.customer-index-index .grid .col-billing_region{width:70px}.adminhtml-cms-hierarchy-index .col-title,.adminhtml-cms-hierarchy-index .col-identifier{max-width:410px;width:410px}.adminhtml-widget-instance-edit .grid-chooser .control{margin-top:-19px;width:80%}.eq-ie9 .adminhtml-widget-instance-edit .grid-chooser .control{margin-top:-18px}.adminhtml-widget-instance-edit .grid-chooser .control .grid-actions{padding:0 0 15px}.adminhtml-widget-instance-edit .grid-chooser .control .grid{padding:0}.adminhtml-widget-instance-edit .grid-chooser .control .addon input:last-child,.adminhtml-widget-instance-edit .grid-chooser .control .addon select:last-child{border-radius:0}.reports-report-product-sold .grid .col-name{max-width:720px;width:720px}.adminhtml-system-store-index .grid td{max-width:310px}.adminhtml-system-currency-index .grid{padding-top:0}.adminhtml-system-currency-index .col-currency-edit-rate{min-width:40px}.adminhtml-system-currency-index .col-base-currency{font-weight:700}.adminhtml-system-currency-index .old-rate{display:block;margin-top:3px;text-align:center}.adminhtml-system-currency-index .hor-scroll{overflow-x:auto;min-width:970px}.adminhtml-system-currencysymbol-index .col-currency{width:35%}.adminhtml-system-currencysymbol-index .grid .input-text{margin:0 10px 0 0;width:50%}.catalog-product-set-index .col-set_name{max-width:930px;width:930px}.adminhtml-export-index .grid td{vertical-align:middle}.adminhtml-export-index .grid .input-text-range{margin:0 10px 0 5px;width:37%}.adminhtml-export-index .grid .input-text-range-date{margin:0 5px;width:32%}.adminhtml-export-index .ui-datepicker-trigger{display:inline-block;margin:-3px 10px 0 0;vertical-align:middle}.adminhtml-notification-index .grid .col-select,.adminhtml-cache-index .grid .col-select,.adminhtml-process-list .grid .col-select,.indexer-indexer-list .grid .col-select{width:10px}@font-face{font-family:'icons-blank-theme';src:url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff2') format('woff2'),url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff') format('woff');font-weight:400;font-style:normal}@font-face{font-family:'icons-blank-theme';src:url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff2') format('woff2'),url('../fonts/Blank-Theme-Icons/Blank-Theme-Icons.woff') format('woff');font-weight:400;font-style:normal}.navigation{background-color:#676056;position:relative;z-index:5}.navigation .level-0.reverse>.submenu{right:1px}.navigation>ul{position:relative;text-align:right}.navigation .level-0>.submenu{display:none;position:absolute;top:100%;padding:19px 13px}.navigation .level-0>.submenu a{display:block;color:#676056;font-size:13px;font-weight:400;line-height:1.385;padding:3px 12px 3px;text-decoration:none}.navigation .level-0>.submenu a:focus,.navigation .level-0>.submenu a:hover{text-decoration:underline}.navigation .level-0>.submenu a:hover{color:#fff;background:#989287;text-decoration:none}.navigation .level-0>.submenu li{margin-bottom:1px}.navigation .level-0>.submenu a[href="#"]{cursor:default;display:block;color:#676056;font-size:14px;font-weight:700;line-height:1;margin:7px 0 6px;padding:0 12px}.navigation .level-0>.submenu a[href="#"]:focus,.navigation .level-0>.submenu a[href="#"]:hover{color:#676056;font-size:14px;font-weight:700;background:0 0;text-decoration:none}.navigation .level-0{display:inline-block;float:left;text-align:left;transition:display .15s ease-out}.navigation .level-0>a{background:0 0;display:block;padding:12px 13px 0;color:#f2ebde;font-size:13px;font-weight:600;text-transform:uppercase;text-decoration:none;transition:background .15s ease-out}.navigation .level-0>a:after{content:"";display:block;margin-top:10px;height:3px;font-size:0}.navigation .level-0.active>a{font-weight:700}.navigation .level-0.active>a:after{background:#ef672f}.navigation .level-0.hover.recent>a{background:#fff;color:#676056;font-size:13px;font-weight:600}.navigation .level-0.hover.recent>a:after{background:0 0}.navigation .level-0.hover.recent.active>a{font-weight:700}.navigation .level-0>.submenu{opacity:0;visibility:hidden}.navigation .level-0.recent.hover>.submenu{opacity:1;visibility:visible}.no-js .navigation .level-0:hover>.submenu,.no-js .navigation .level-0.hover>.submenu,.no-js .navigation .level-0>a:focus+.submenu{display:block}.navigation .level-0>.submenu{background:#fff;box-shadow:0 3px 3px rgba(50,50,50,.15)}.navigation .level-0>.submenu li{max-width:200px}.navigation .level-0>.submenu>ul{white-space:nowrap}.navigation .level-0>.submenu .column{display:inline-block;margin-left:40px;vertical-align:top}.navigation .level-0>.submenu .column:first-child{margin-left:0}.navigation .level-0 .submenu .level-1{white-space:normal}.navigation .level-0.parent .submenu .level-1.parent{margin:17px 0 25px}.navigation .level-0.parent .level-1.parent:first-child{margin-top:0}.navigation .level-2 .submenu{margin-left:7px}.navigation .level-0>.submenu .level-2>a[href="#"]{font-size:13px;margin-top:10px;margin-left:7px}.navigation .level-2>.submenu a{font-size:12px;line-height:1.231}.navigation .level-0>.submenu .level-3>a[href="#"],.navigation .level-3 .submenu{margin-left:15px}.navigation .level-0.item-system,.navigation .level-0.item-stores{float:none}.navigation .level-0.item-system>.submenu,.navigation .level-0.item-stores>.submenu{left:auto;right:1px}.adminhtml-dashboard-index .col-1-layout{max-width:1300px;border:none;border-radius:0;padding:0;background:#f7f3eb}.dashboard-inner{padding-top:35px}.dashboard-inner:before,.dashboard-inner:after{content:"";display:table}.dashboard-inner:after{clear:both}.dashboard-inner:before,.dashboard-inner:after{content:"";display:table}.dashboard-inner:after{clear:both}.dashboard-secondary{float:left;width:32%;margin:0 1.5%}.dashboard-main{float:right;width:65%}.dashboard-diagram-chart{max-width:100%;height:auto}.dashboard-diagram-nodata,.dashboard-diagram-switcher{padding:20px 0}.dashboard-diagram-image{background:#fff url(../mui/images/ajax-loader-small.gif) no-repeat 50% 50%}.dashboard-container .ui-tabs-panel{background-color:#fff;min-height:40px;padding:15px}.dashboard-store-stats{margin-top:35px}.dashboard-store-stats .ui-tabs-panel{background:#fff url(../mui/images/ajax-loader-small.gif) no-repeat 50% 50%}.dashboard-item{margin-bottom:30px}.dashboard-item-header{margin-left:5px}.dashboard-item.dashboard-item-primary{margin-bottom:35px}.dashboard-item.dashboard-item-primary .title{font-size:22px;margin-bottom:5px}.dashboard-item.dashboard-item-primary .dashboard-sales-value{display:block;text-align:right;font-weight:600;font-size:30px;margin-right:12px;padding-bottom:5px}.dashboard-item.dashboard-item-primary:first-child{color:#ef672f}.dashboard-item.dashboard-item-primary:first-child .title{color:#ef672f}.dashboard-totals{background:#fff;padding:50px 15px 25px}.dashboard-totals-list{margin:0;padding:0;list-style:none none}.dashboard-totals-list:before,.dashboard-totals-list:after{content:"";display:table}.dashboard-totals-list:after{clear:both}.dashboard-totals-list:before,.dashboard-totals-list:after{content:"";display:table}.dashboard-totals-list:after{clear:both}.dashboard-totals-item{float:left;width:18%;margin-left:7%;padding-top:15px;border-top:2px solid #cac3b4}.dashboard-totals-item:first-child{margin-left:0}.dashboard-totals-label{display:block;font-size:16px;font-weight:600;padding-bottom:2px}.dashboard-totals-value{color:#ef672f;font-size:20px}.dashboard-data{width:100%}.dashboard-data thead{background:0 0}.dashboard-data thead tr{background:0 0}.dashboard-data th,.dashboard-data td{border:none;padding:10px 12px;text-align:right}.dashboard-data th:first-child,.dashboard-data td:first-child{text-align:left}.dashboard-data th{color:#676056;font-weight:600}.dashboard-data td{background-color:transparent}.dashboard-data tbody tr:hover td{background-color:transparent}.dashboard-data tbody tr:nth-child(odd) td,.dashboard-data tbody tr:nth-child(odd):hover td,.dashboard-data tbody tr:nth-child(odd) th,.dashboard-data tbody tr:nth-child(odd):hover th{background-color:#e1dbcf}.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd) td,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd):hover td,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd) th,.ui-tabs-panel .dashboard-data tbody tr:nth-child(odd):hover th{background-color:#f7f3eb}.dashboard-data td.empty-text{text-align:center}.ui-tabs-panel .dashboard-data{background-color:#fff}.mage-dropdown-dialog.ui-dialog .ui-dialog-content{overflow:visible}.mage-dropdown-dialog.ui-dialog .ui-dialog-buttonpane{padding:0}.message-system-inner{background:#f7f3eb;border:1px solid #c0bbaf;border-top:0;border-radius:0 0 5px 5px;float:right;overflow:hidden}.message-system-unread .message-system-inner{float:none}.message-system-list{margin:0;padding:0;list-style:none;float:left}.message-system .message-system-list{width:75%}.message-system-list li{padding:5px 13px 7px 36px;position:relative}.message-system-short{padding:5px 13px 7px;float:right}.message-system-short span{display:inline-block;margin-left:7px;border-left:1px #d1ccc3 solid}.message-system-short span:first-child{border:0;margin-left:0}.message-system-short a{padding-left:27px;position:relative;height:16px}.message-system .message-system-short a:before,.message-system-list li:before{font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;height:16px;width:16px;font-size:16px;line-height:16px;text-align:center;position:absolute;left:7px;top:2px}.message-system-list li:before{top:5px;left:13px}.message-system .message-system-short .warning a:before,.message-system-list li.warning:before{content:"\e006";color:#f2a825}.message-system .message-system-short .error a:before,.message-system-list li.error:before{content:"\e086";font-family:'MUI-Icons';color:#c00815}.ui-dialog .message-system-list{margin-bottom:25px}.sales-order-create-index .order-errors .notice{color:#ed4f2e;font-size:11px;margin:5px 0 0}.order-errors .fieldset-wrapper-title .title{box-sizing:border-box;background:#fffbf0;border:1px solid #d87e34;border-radius:5px;color:#676056;font-size:14px;margin:20px 0;padding:10px 26px 10px 35px;position:relative}.order-errors .fieldset-wrapper-title .title:before{position:absolute;left:11px;top:50%;margin-top:-11px;width:auto;height:auto;font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;font-size:16px;line-height:inherit;content:'\e046';color:#d87e34}.search-global.miniform{position:relative;z-index:1000;display:inline-block;vertical-align:top;margin:6px 10px 0}.search-global.miniform .mage-suggest{border:0;border-radius:0}.search-global-actions{display:none}.search-global-field{margin:0}.search-global-field .label{position:absolute;right:4px;z-index:2;cursor:pointer;display:inline-block;text-decoration:none}.search-global-field .label>span{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.search-global-field .label>span.focusable:active,.search-global-field .label>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.search-global-field .label>span.focusable:active,.search-global-field .label>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.search-global-field .label:before{font-family:'MUI-Icons';content:"\e01f";font-size:18px;line-height:29px;color:#cac3b4;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.search-global-field .control{width:48px;overflow:hidden;opacity:0;transition:all .3s ease}.search-global-field .control input[type=text]{background:0 0;border:none;width:100%}.search-global-field.active{z-index:2}.search-global-field.active .label:before{display:none}.search-global-field.active .control{overflow:visible;opacity:1;transition:all .3s ease;width:300px}.search-global-menu{box-sizing:border-box;display:block;width:100%}.notifications-summary{display:inline-block;text-align:left;position:relative;z-index:1}.notifications-summary.active{z-index:999}.notifications-action{color:#f2ebde;padding:12px 22px 11px;text-transform:capitalize;display:inline-block;text-decoration:none}.notifications-action:before{font-family:"MUI-Icons";content:"\e06e";font-size:18px;line-height:18px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-action:visited,.notifications-action:focus,.notifications-action:active,.notifications-action:hover{color:#f2ebde;text-decoration:none}.notifications-action.active{background-color:#fff;color:#676056}.notifications-action .text{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-action .text.focusable:active,.notifications-action .text.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-action .text.focusable:active,.notifications-action .text.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-action .qty.counter{display:inline-block;background:#ed4f2e;color:#f2ebde;font-size:12px;line-height:12px;font-weight:700;padding:1px 3px;position:absolute;top:6px;left:50%;border-radius:4px}.notifications-list{width:300px;padding:0;margin:0}.notifications-list .last{padding:10px;text-align:center;font-size:12px}.notifications-summary .notifications-entry{padding:15px;color:#676056;font-size:11px;font-weight:400}.notifications-entry{position:relative;z-index:1}.notifications-entry:hover .action{display:block}.notifications-entry-title{padding-right:15px;color:#ed4f2e;font-size:12px;font-weight:600;display:block;margin-bottom:10px}.notifications-entry-description{line-height:1.3;display:block;max-height:3.9em;overflow:hidden;margin-bottom:10px;text-overflow:ellipsis}.notifications-close.action{position:absolute;z-index:1;top:12px;right:12px;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400;display:none}.notifications-close.action>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-close.action>span.focusable:active,.notifications-close.action>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-close.action>span.focusable:active,.notifications-close.action>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-close.action:before{font-family:'MUI-Icons';content:"\e07f";font-size:16px;line-height:inherit;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-close.action:focus,.notifications-close.action:active{background:0 0;border:none}.notifications-close.action:hover{background:0 0;border:none}.notifications-close.action.disabled,.notifications-close.action[disabled],fieldset[disabled] .notifications-close.action{cursor:not-allowed;pointer-events:none;opacity:.5}.notifications-dialog-content{display:none}.notifications-critical .notifications-entry-title{padding-left:25px;display:inline-block;text-decoration:none}.notifications-critical .notifications-entry-title:before{font-family:'MUI-Icons';content:"\e086";font-size:18px;line-height:18px;color:#c00815;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.notifications-critical .notifications-entry-title:before{position:absolute;margin-left:-25px}.notifications-dialog-content .notifications-entry-time{color:#8c867e;font-size:13px;font-family:Helvetica,Arial,sans-serif;position:absolute;right:17px;bottom:27px;text-align:right}.notifications-url{display:inline-block;text-decoration:none}.notifications-url>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.notifications-url>span.focusable:active,.notifications-url>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-url>span.focusable:active,.notifications-url>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.notifications-url:after{font-family:'MUI-Icons';content:"\e084";font-size:16px;line-height:inherit;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-2px 0 0 10px}.notifications-dialog-content .notifications-entry-title{font-size:15px}.locale-switcher-field{white-space:nowrap;float:left}.locale-switcher-field .control,.locale-switcher-field .label{vertical-align:middle;margin:0 10px 0 0;display:inline-block}.locale-switcher-select{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;border:1px solid #ada89e;max-width:200px;height:31px;background:url("../images/select-bg.svg") no-repeat 100% 50%;background-size:30px 60px;padding-right:29px;text-indent:.01px;text-overflow:''}.locale-switcher-select::-ms-expand{display:none}.lt-ie10 .locale-switcher-select{background-image:none;padding-right:4px}@-moz-document url-prefix(){.locale-switcher-select{background-image:none}}@-moz-document url-prefix(){.locale-switcher-select{background-image:none}}.mage-suggest{text-align:left;box-sizing:border-box;position:relative;display:inline-block;vertical-align:top;width:100%;background-color:#fff;border:1px solid #ada89e;border-radius:2px}.mage-suggest:after{position:absolute;top:3px;right:3px;bottom:0;width:22px;text-align:center;font-family:'MUI-Icons';font-style:normal;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;content:'\e01f';font-size:18px;color:#b2b2b2}.mage-suggest input[type=search],.mage-suggest input.search{width:100%;border:none;background:0 0;padding-right:30px}.mage-suggest.category-select input[type=search],.mage-suggest.category-select input.search{height:26px}.mage-suggest-dropdown{position:absolute;left:0;right:0;top:100%;margin:1px -1px 0;border:1px solid #cac2b5;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);z-index:990}.mage-suggest-dropdown ul{margin:0;padding:0;list-style:none}.mage-suggest-dropdown li{border-bottom:1px solid #e5e5e5;padding:0}.mage-suggest-dropdown li a{display:block}.mage-suggest-dropdown li a.ui-state-focus{background:#f5f5f5}.mage-suggest-dropdown li a,.mage-suggest-dropdown .jstree li a:hover,.mage-suggest-dropdown .jstree .jstree-hovered,.mage-suggest-dropdown .jstree .jstree-clicked{padding:6px 12px 5px;text-decoration:none;color:#333}.mage-suggest-dropdown .jstree li a:hover,.mage-suggest-dropdown .jstree .jstree-hovered,.mage-suggest-dropdown .jstree .jstree-clicked{border:none}.mage-suggest-dropdown .jstree li{border-bottom:0}.mage-suggest-dropdown .jstree li a{display:inline-block}.mage-suggest-dropdown .jstree .mage-suggest-selected>a{color:#000;background:#f1ffeb}.field-category_ids .mage-suggest-dropdown,.field-new_category_parent .mage-suggest-dropdown{max-height:200px;overflow:auto}.mage-suggest-dropdown .jstree .mage-suggest-selected>a:hover,.mage-suggest-dropdown .jstree .mage-suggest-selected>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-selected>.jstree-clicked,.mage-suggest-dropdown .jstree .mage-suggest-selected.mage-suggest-not-active>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-selected.mage-suggest-not-active>.jstree-clicked{background:#e5ffd9}.mage-suggest-dropdown .jstree .mage-suggest-not-active>a{color:#d4d4d4}.mage-suggest-dropdown .jstree .mage-suggest-not-active>a:hover,.mage-suggest-dropdown .jstree .mage-suggest-not-active>.jstree-hovered,.mage-suggest-dropdown .jstree .mage-suggest-not-active>.jstree-clicked{background:#f5f5f5}.mage-suggest-dropdown .category-path{font-size:11px;margin-left:10px;color:#9ba8b5}.suggest-expandable .action-dropdown .action-toggle{display:inline-block;max-width:500px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:none;box-shadow:none;color:#676056;font-size:12px;padding:5px 4px;filter:none}.suggest-expandable .action-dropdown .action-toggle span{display:inline}.suggest-expandable .action-dropdown .action-toggle:before{display:inline-block;float:right;margin-left:4px;font-size:13px;color:#b2b0ad}.suggest-expandable .action-dropdown .action-toggle:hover:before{color:#7e7e7e}.suggest-expandable .dropdown-menu{margin:1px 0 0;left:0;right:auto;width:245px;z-index:4}.suggest-expandable .mage-suggest{border:none;border-radius:3px 3px 0 0}.suggest-expandable .mage-suggest:after{top:10px;right:8px}.suggest-expandable .mage-suggest-inner .title{margin:0;padding:0 10px 4px;text-transform:uppercase;color:#a6a098;font-size:12px;border-bottom:1px solid #e5e5e5}.suggest-expandable .mage-suggest-inner>input[type=search],.suggest-expandable .mage-suggest-inner>input.search{position:relative;margin:6px 5px 5px;padding-right:20px;border:1px solid #ada89e;width:236px;z-index:1}.suggest-expandable .mage-suggest-inner>input.ui-autocomplete-loading,.suggest-expandable .mage-suggest-inner>input.mage-suggest-state-loading{background:#fff url("../mui/images/ajax-loader-small.gif") no-repeat 190px 50%}.suggest-expandable .mage-suggest-dropdown{margin-top:0;border-top:0;border-radius:0 0 3px 3px;max-height:300px;overflow:auto;width:100%;float:left}.suggest-expandable .mage-suggest-dropdown ul{margin:0;padding:0;list-style:none}.suggest-expandable .action-show-all:hover,.suggest-expandable .action-show-all:active,.suggest-expandable .action-show-all:focus,.suggest-expandable .action-show-all[disabled]{border-top:1px solid #e5e5e5;display:block;width:100%;padding:8px 10px 10px;text-align:left;font:12px/1.333 Arial,Verdana,sans-serif;color:#676056}.product-actions .suggest-expandable{max-width:500px;float:left;margin-top:1px}.page-actions.fixed #product-template-suggest-container{display:none}.catalog-category-edit .col-2-left-layout:before{display:none}.category-content .ui-tabs-panel .fieldset{padding-top:40px}.category-content .ui-tabs-panel .fieldset .legend{display:none}.attributes-edit-form .field:not(.field-weight) .addon{display:block;position:relative}.attributes-edit-form .field:not(.field-weight) .addon input[type=text]{border-width:1px}.attributes-edit-form .field:not(.field-weight) .addon .addafter{display:block;border:0;height:auto;width:auto}.attributes-edit-form .field:not(.field-weight) .addon input:focus~.addafter{box-shadow:none}.attributes-edit-form .with-addon .textarea{margin:0}.attributes-edit-form .attribute-change-checkbox{display:block;margin-top:5px}.attributes-edit-form .attribute-change-checkbox .label{float:none;padding:0;width:auto}.attributes-edit-form .attribute-change-checkbox .checkbox{margin-right:5px;width:auto}.attributes-edit-form .field-price .addon>input,.attributes-edit-form .field-special_price .addon>input,.attributes-edit-form .field-gift_wrapping_price .addon>input,.attributes-edit-form .field-msrp .addon>input,.attributes-edit-form .field-gift_wrapping_price .addon>input{padding-left:23px}.attributes-edit-form .field-price .addafter>strong,.attributes-edit-form .field-special_price .addafter>strong,.attributes-edit-form .field-gift_wrapping_price .addafter>strong,.attributes-edit-form .field-msrp .addafter>strong,.attributes-edit-form .field-gift_wrapping_price .addafter>strong{left:5px;position:absolute;top:3px}.attributes-edit-form .field.type-price input:focus+label,.attributes-edit-form .field-price input:focus+label,.attributes-edit-form .field-special_price input:focus+label,.attributes-edit-form .field-msrp input:focus+label,.attributes-edit-form .field-weight input:focus+label{box-shadow:none}.attributes-edit-form .field-special_from_date>.control .input-text,.attributes-edit-form .field-special_to_date>.control .input-text,.attributes-edit-form .field-news_from_date>.control .input-text,.attributes-edit-form .field-news_to_date>.control .input-text,.attributes-edit-form .field-custom_design_from>.control .input-text,.attributes-edit-form .field-custom_design_to>.control .input-text{border-width:1px;width:130px}.attributes-edit-form .field-weight .fields-group-2 .control{padding-right:27px}.attributes-edit-form .field-weight .fields-group-2 .control .addafter+.addafter{border-width:1px 1px 1px 0;border-style:solid;height:28px;right:0;position:absolute;top:0}.attributes-edit-form .field-weight .fields-group-2 .control .addafter strong{line-height:28px}.attributes-edit-form .field-weight .fields-group-2 .control>input:focus+.addafter+.addafter{box-shadow:0 0 8px rgba(82,168,236,.6)}.attributes-edit-form .field-gift_message_available .addon>input[type=checkbox],.attributes-edit-form .field-gift_wrapping_available .addon>input[type=checkbox]{width:auto;margin-right:5px}.attributes-edit-form .fieldset>.addafter{display:none}.advanced-inventory-edit .field.choice{display:block;margin:3px 0 0}.advanced-inventory-edit .field.choice .label{padding-top:1px}.product-actions:before,.product-actions:after{content:"";display:table}.product-actions:after{clear:both}.product-actions:before,.product-actions:after{content:"";display:table}.product-actions:after{clear:both}.product-actions .switcher{float:right}#configurable-attributes-container .actions-select{display:inline-block;position:relative}#configurable-attributes-container .actions-select:before,#configurable-attributes-container .actions-select:after{content:"";display:table}#configurable-attributes-container .actions-select:after{clear:both}#configurable-attributes-container .actions-select:before,#configurable-attributes-container .actions-select:after{content:"";display:table}#configurable-attributes-container .actions-select:after{clear:both}#configurable-attributes-container .actions-select .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}#configurable-attributes-container .actions-select .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:22px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#configurable-attributes-container .actions-select .action.toggle:hover:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle:active:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle.active{display:inline-block;text-decoration:none}#configurable-attributes-container .actions-select .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:22px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#configurable-attributes-container .actions-select .action.toggle.active:hover:after{color:inherit}#configurable-attributes-container .actions-select .action.toggle.active:active:after{color:inherit}#configurable-attributes-container .actions-select ul.dropdown{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:100%;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}#configurable-attributes-container .actions-select ul.dropdown li{margin:0;padding:3px 5px}#configurable-attributes-container .actions-select ul.dropdown li:hover{background:#e8e8e8;cursor:pointer}#configurable-attributes-container .actions-select.active{overflow:visible}#configurable-attributes-container .actions-select.active ul.dropdown{display:block}#configurable-attributes-container .actions-select .action.toggle{padding:1px 8px;border:1px solid #ada89e;background:#fff;border-radius:0 2px 2px 0}#configurable-attributes-container .actions-select .action.toggle:after{width:14px;text-indent:-2px}#configurable-attributes-container .actions-select ul.dropdown li:hover{background:#eef8fc}#configurable-attributes-container .actions-select ul.dropdown a{color:#333;text-decoration:none}#product-variations-matrix .actions-image-uploader{position:relative;display:block;width:50px}#product-variations-matrix .actions-image-uploader:before,#product-variations-matrix .actions-image-uploader:after{content:"";display:table}#product-variations-matrix .actions-image-uploader:after{clear:both}#product-variations-matrix .actions-image-uploader:before,#product-variations-matrix .actions-image-uploader:after{content:"";display:table}#product-variations-matrix .actions-image-uploader:after{clear:both}#product-variations-matrix .actions-image-uploader .action.split{float:left;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle{float:right;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle{padding:6px 5px;display:inline-block;text-decoration:none}#product-variations-matrix .actions-image-uploader .action.toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle:hover:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle:active:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle.active{display:inline-block;text-decoration:none}#product-variations-matrix .actions-image-uploader .action.toggle.active>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:active,#product-variations-matrix .actions-image-uploader .action.toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}#product-variations-matrix .actions-image-uploader .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}#product-variations-matrix .actions-image-uploader .action.toggle.active:hover:after{color:inherit}#product-variations-matrix .actions-image-uploader .action.toggle.active:active:after{color:inherit}#product-variations-matrix .actions-image-uploader ul.dropdown{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:100%;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}#product-variations-matrix .actions-image-uploader ul.dropdown li{margin:0;padding:3px 5px}#product-variations-matrix .actions-image-uploader ul.dropdown li:hover{background:#e8e8e8;cursor:pointer}#product-variations-matrix .actions-image-uploader.active{overflow:visible}#product-variations-matrix .actions-image-uploader.active ul.dropdown{display:block}#product-variations-matrix .actions-image-uploader .action.toggle{padding:0 2px;border:1px solid #b7b2a7;background:#fff;border-radius:0 4px 4px 0;border-left:none;height:33px}#product-variations-matrix .actions-image-uploader .action.toggle.no-display{display:none}#product-variations-matrix .actions-image-uploader .action.toggle:after{width:12px;text-indent:-5px}#product-variations-matrix .actions-image-uploader ul.dropdown{left:0;margin-left:0;width:100px}#product-variations-matrix .actions-image-uploader ul.dropdown li:hover{background:#eef8fc}#product-variations-matrix .actions-image-uploader ul.dropdown a{color:#333;text-decoration:none}.debugging-hints .page-actions{position:relative;z-index:1}.debugging-hints .page-actions .debugging-hint-template-file{left:auto !important;right:0 !important}.filter-segments{list-style:none;padding:0}.adminhtml-report-customer-test-detail .col-id{width:35px}.adminhtml-report-customer-test-detail .col-period{white-space:nowrap;width:70px}.adminhtml-report-customer-test-detail .col-zip{width:50px}.adminhtml-report-customer-test-segment .col-id{width:35px}.adminhtml-report-customer-test-segment .col-status{width:65px}.adminhtml-report-customer-test-segment .col-qty{width:145px}.adminhtml-report-customer-test-segment .col-segment,.adminhtml-report-customer-test-segment .col-website{width:35%}.adminhtml-report-customer-test-segment .col-select{width:45px}.test-custom-attributes{margin-bottom:20px}.adminhtml-test-index th.col-id{text-align:left}.adminhtml-test-index .col-price{text-align:right;width:50px}.adminhtml-test-index .col-actions{width:50px}.adminhtml-test-index .col-select{width:60px}.adminhtml-test-edit .field-image .control{line-height:28px}.adminhtml-test-edit .field-image a{display:inline-block;margin:0 5px 0 0}.adminhtml-test-edit .field-image img{vertical-align:middle}.adminhtml-test-new .field-image .input-file,.adminhtml-test-edit .field-image .input-file{display:inline-block;margin:0 15px 0 0;width:auto}.adminhtml-test-new .field-image .addafter,.adminhtml-test-edit .field-image .addafter{border:0;box-shadow:none;display:inline-block;margin:0 15px 0 0;height:auto;width:auto}.adminhtml-test-new .field-image .delete-image,.adminhtml-test-edit .field-image .delete-image{display:inline-block;white-space:nowrap}.adminhtml-test-edit .field-image .delete-image input{margin:-3px 5px 0 0;width:auto;display:inline-block}.adminhtml-test-edit .field-image .addon .delete-image input:focus+label{border:0;box-shadow:none}.adminhtml-test-index .col-id{width:35px}.adminhtml-test-index .col-status{white-space:normal;width:75px}.adminhtml-test-index .col-websites{white-space:nowrap;width:200px}.adminhtml-test-index .col-price .label{display:inline-block;min-width:60px;white-space:nowrap}.adminhtml-test-index .col-price .price-excl-tax .price,.adminhtml-test-index .col-price .price-incl-tax .price{font-weight:700}.invitee_information,.inviter_information{width:48.9362%}.invitee_information{float:left}.inviter_information{float:right}.test_information .data-table th,.invitee_information .data-table th,.inviter_information .data-table th{width:20%;white-space:nowrap}.test_information .data-table textarea,.test_information .data-table input{width:100%}.tests-history ul{margin:0;padding-left:25px}.tests-history ul .status:before{display:inline-block;content:"|";margin:0 10px}.adminhtml-report-test-order .col-period{white-space:nowrap;width:70px}.adminhtml-report-test-order .col-inv-sent,.adminhtml-report-test-order .col-inv-acc,.adminhtml-report-test-order .col-acc,.adminhtml-report-test-order .col-rate{text-align:right;width:23%}.adminhtml-report-test-customer .col-id{width:35px}.adminhtml-report-test-customer .col-period{white-space:nowrap;width:70px}.adminhtml-report-test-customer .col-inv-sent,.adminhtml-report-test-customer .col-inv-acc{text-align:right;width:120px}.adminhtml-report-test-index .col-period{white-space:nowrap}.adminhtml-report-test-index .col-inv-sent,.adminhtml-report-test-index .col-inv-acc,.adminhtml-report-test-index .col-inv-disc,.adminhtml-report-test-index .col-inv-acc-rate,.adminhtml-report-test-index .col-inv-disc-rate{text-align:right;width:19%}.test_information .data-table,.invitee_information .data-table,.inviter_information .data-table{width:100%}.test_information .data-table tbody tr th,.invitee_information .data-table tbody tr th,.inviter_information .data-table tbody tr th{font-weight:700}.test_information .data-table tbody tr td,.test_information .data-table tbody tr th,.invitee_information .data-table tbody tr td,.invitee_information .data-table tbody tr th,.inviter_information .data-table tbody tr td,.inviter_information .data-table tbody tr th{background-color:#fff;border:0;padding:9px 10px 10px;color:#666;vertical-align:top}.test_information .data-table tbody tr:nth-child(2n+1) td,.test_information .data-table tbody tr:nth-child(2n+1) th,.invitee_information .data-table tbody tr:nth-child(2n+1) td,.invitee_information .data-table tbody tr:nth-child(2n+1) th,.inviter_information .data-table tbody tr:nth-child(2n+1) td,.inviter_information .data-table tbody tr:nth-child(2n+1) th{background-color:#fbfaf6}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table .col-sort-order{width:80px}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td{vertical-align:top}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td select,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td select{display:block;width:100%}[class^=" adminhtml-test-"] .fieldset-wrapper-content .data-table td .input-radio.global-scope,[class^=" adminhtml-test-"] .fieldset-wrapper-content .accordion .config .data-table td .input-radio.global-scope{margin-top:9px}.sales-order-create-index .ui-dialog .content>.test .field.text .input-text{width:100%}.sales-order-create-index .ui-dialog .content>.test .note .price{font-weight:600}.sales-order-create-index .ui-dialog .content>.test .note .price:before{content:": "}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .label:after{content:": "}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .control{display:inline-block;font-weight:600}.sales-order-create-index .ui-dialog .content>.test .fixed.amount .control .control-value{margin:-2px 0 0;padding:0}.eq-ie9 [class^=" adminhtml-test-"] .custom-options .data-table{word-wrap:normal;table-layout:auto}.rma-items .col-actions a.disabled,.newRma .col-actions a.disabled{cursor:default;opacity:.5}.rma-items .col-actions a.disabled:hover,.newRma .col-actions a.disabled:hover{text-decoration:none}.block.mselect-list .mselect-input{width:100%}.block.mselect-list .mselect-input-container .mselect-save{top:4px}.block.mselect-list .mselect-input-container .mselect-cancel{top:4px}html{font-size:62.5%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-size-adjust:100%}body,html{height:100%;min-height:100%}body{color:#676056;font-family:'Open Sans',sans-serif;line-height:1.33;font-weight:400;font-size:1.4rem;background:#f2ebde;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}body>*{-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0;-webkit-flex-basis:auto;flex-basis:auto}.page-wrapper{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-height:100%;width:100%;max-width:100%;min-width:990px}.page-wrapper>*{-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0;-webkit-flex-basis:auto;flex-basis:auto}.page-header{text-align:right}.page-header-wrapper{background-color:#31302b}.page-header:after{content:"";display:table;clear:both}.page-header .logo{margin-top:5px;float:left;text-decoration:none;display:inline-block}.page-header .logo:before{content:"";display:inline-block;vertical-align:middle;width:109px;height:35px;background-image:url("../images/logo.svg");background-size:109px 70px;background-repeat:no-repeat}.page-header .logo:after{display:inline-block;vertical-align:middle;margin-left:10px;content:attr(data-edition);font-weight:600;font-size:16px;color:#ef672f;margin-top:-2px}.page-header .logo span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.page-header .logo span.focusable:active,.page-header .logo span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.page-header .logo span.focusable:active,.page-header .logo span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.page-header .dropdown-menu{border:0}.admin-user{display:inline-block;vertical-align:top;position:relative;text-align:left}.admin-user-account{text-decoration:none;display:inline-block;padding:12px 14px;color:#f2ebde}.admin-user-account:after{font-family:"MUI-Icons";content:"\e02c";font-size:13px;line-height:13px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-3px 0 0}.admin-user-account:link,.admin-user-account:visited{color:#f2ebde}.admin-user-account:focus,.admin-user-account:active,.admin-user-account:hover{color:#f2ebde;text-decoration:none}.active .admin-user-account{background-color:#fff;color:#676056}.admin-user-menu{padding:15px;white-space:nowrap;margin-top:0}.admin-user-menu li{border:0;padding:0}.admin-user-menu li:hover{background:0 0}.admin-user-menu a{display:block;color:#676056;font-size:13px;font-weight:400;line-height:1.385;padding:3px 12px 3px;text-decoration:none}.admin-user-menu a:focus,.admin-user-menu a:hover{text-decoration:underline}.admin-user-menu a:hover{color:#fff;background:#989287;text-decoration:none}.admin-user-menu a span:before{content:"("}.admin-user-menu a span:after{content:")"}.page-actions.fixed .page-actions-buttons{padding-right:15px}.page-main-actions{background:#e0dace;color:#645d53;padding:15px;margin-left:auto;margin-right:auto;box-sizing:border-box}.page-main-actions:before,.page-main-actions:after{content:"";display:table}.page-main-actions:after{clear:both}.page-main-actions:before,.page-main-actions:after{content:"";display:table}.page-main-actions:after{clear:both}.page-main-actions .page-actions{float:right}.page-main-actions .page-actions .page-actions-buttons{float:right;display:-webkit-flex;display:-ms-flexbox;display:flex;justify-content:flex-end}.page-main-actions .page-actions button,.page-main-actions .page-actions .action-add.mselect-button-add{margin-left:13px}.page-main-actions .page-actions button.primary,.page-main-actions .page-actions .action-add.mselect-button-add.primary{float:right;-ms-flex-order:2;-webkit-order:2;order:2}.page-main-actions .page-actions button.save:not(.primary),.page-main-actions .page-actions .action-add.mselect-button-add.save:not(.primary){float:right;-ms-flex-order:1;-webkit-order:1;order:1}.page-main-actions .page-actions button.back,.page-main-actions .page-actions button.action-back,.page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.back,.page-main-actions .page-actions .action-add.mselect-button-add.action-back,.page-main-actions .page-actions .action-add.mselect-button-add.delete{background-image:none;background:0 0;border:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400;margin:0 13px}.page-main-actions .page-actions button.back:focus,.page-main-actions .page-actions button.action-back:focus,.page-main-actions .page-actions button.delete:focus,.page-main-actions .page-actions button.back:active,.page-main-actions .page-actions button.action-back:active,.page-main-actions .page-actions button.delete:active,.page-main-actions .page-actions .action-add.mselect-button-add.back:focus,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:focus,.page-main-actions .page-actions .action-add.mselect-button-add.delete:focus,.page-main-actions .page-actions .action-add.mselect-button-add.back:active,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:active,.page-main-actions .page-actions .action-add.mselect-button-add.delete:active{background:0 0;border:none}.page-main-actions .page-actions button.back:hover,.page-main-actions .page-actions button.action-back:hover,.page-main-actions .page-actions button.delete:hover,.page-main-actions .page-actions .action-add.mselect-button-add.back:hover,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:hover,.page-main-actions .page-actions .action-add.mselect-button-add.delete:hover{background:0 0;border:none}.page-main-actions .page-actions button.back.disabled,.page-main-actions .page-actions button.action-back.disabled,.page-main-actions .page-actions button.delete.disabled,.page-main-actions .page-actions button.back[disabled],.page-main-actions .page-actions button.action-back[disabled],.page-main-actions .page-actions button.delete[disabled],fieldset[disabled] .page-main-actions .page-actions button.back,fieldset[disabled] .page-main-actions .page-actions button.action-back,fieldset[disabled] .page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.back.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.action-back.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.delete.disabled,.page-main-actions .page-actions .action-add.mselect-button-add.back[disabled],.page-main-actions .page-actions .action-add.mselect-button-add.action-back[disabled],.page-main-actions .page-actions .action-add.mselect-button-add.delete[disabled],fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.back,fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.action-back,fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-add.delete{cursor:not-allowed;pointer-events:none;opacity:.5}.ie .page-main-actions .page-actions button.back,.ie .page-main-actions .page-actions button.action-back,.ie .page-main-actions .page-actions button.delete,.ie .page-main-actions .page-actions .action-add.mselect-button-add.back,.ie .page-main-actions .page-actions .action-add.mselect-button-add.action-back,.ie .page-main-actions .page-actions .action-add.mselect-button-add.delete{margin-top:6px}.page-main-actions .page-actions button.delete,.page-main-actions .page-actions .action-add.mselect-button-add.delete{color:#e22626;float:left;-ms-flex-order:-1;-webkit-order:-1;order:-1}.page-main-actions .page-actions button.back,.page-main-actions .page-actions button.action-back,.page-main-actions .page-actions .action-add.mselect-button-add.back,.page-main-actions .page-actions .action-add.mselect-button-add.action-back{float:left;-ms-flex-order:-1;-webkit-order:-1;order:-1;display:inline-block;text-decoration:none}.page-main-actions .page-actions button.back:before,.page-main-actions .page-actions button.action-back:before,.page-main-actions .page-actions .action-add.mselect-button-add.back:before,.page-main-actions .page-actions .action-add.mselect-button-add.action-back:before{font-family:'icons-blank-theme';content:'\e625';font-size:inherit;line-height:normal;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:0 2px 0 0}.page-main-actions .page-actions .actions-split{margin-left:13px;float:right;-ms-flex-order:2;-webkit-order:2;order:2}.page-main-actions .page-actions .actions-split button.primary,.page-main-actions .page-actions .actions-split .action-add.mselect-button-add.primary{float:left}.page-main-actions .page-actions .actions-split .dropdown-menu{text-align:left}.page-main-actions .page-actions .actions-split .dropdown-menu .item{display:block}.page-main-actions .page-actions.fixed{position:fixed;top:0;left:0;right:0;z-index:10;padding:0;background:-webkit-linear-gradient(top,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:-ms-linear-gradient(top,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:linear-gradient(to bottom,#f5f2ed 0%,#f5f2ed 56%,rgba(245,242,237,0) 100%);background:#e0dace}.page-main-actions .page-actions.fixed .page-actions-inner{position:relative;padding-top:15px;padding-bottom:15px;min-height:36px;text-align:right;box-sizing:border-box}.page-main-actions .page-actions.fixed .page-actions-inner:before,.page-main-actions .page-actions.fixed .page-actions-inner:after{content:"";display:table}.page-main-actions .page-actions.fixed .page-actions-inner:after{clear:both}.page-main-actions .page-actions.fixed .page-actions-inner:before,.page-main-actions .page-actions.fixed .page-actions-inner:after{content:"";display:table}.page-main-actions .page-actions.fixed .page-actions-inner:after{clear:both}.page-main-actions .page-actions.fixed .page-actions-inner:before{text-align:left;content:attr(data-title);float:left;font-size:20px;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lt-ie10 .page-main-actions .page-actions.fixed .page-actions-inner{background:#f5f2ed}.page-main-actions .store-switcher{margin-top:5px}.store-switcher{display:inline-block;font-size:13px}.store-switcher .label{margin-right:5px}.store-switcher .actions.dropdown{display:inline-block;position:relative}.store-switcher .actions.dropdown:before,.store-switcher .actions.dropdown:after{content:"";display:table}.store-switcher .actions.dropdown:after{clear:both}.store-switcher .actions.dropdown:before,.store-switcher .actions.dropdown:after{content:"";display:table}.store-switcher .actions.dropdown:after{clear:both}.store-switcher .actions.dropdown .action.toggle{cursor:pointer;display:inline-block;text-decoration:none}.store-switcher .actions.dropdown .action.toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:20px;color:#645d53;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.store-switcher .actions.dropdown .action.toggle:hover:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle:active:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle.active{display:inline-block;text-decoration:none}.store-switcher .actions.dropdown .action.toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:20px;color:#645d53;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.store-switcher .actions.dropdown .action.toggle.active:hover:after{color:#645d53}.store-switcher .actions.dropdown .action.toggle.active:active:after{color:#645d53}.store-switcher .actions.dropdown .dropdown-menu{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px #ada89e solid;position:absolute;z-index:100;top:100%;min-width:195px;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}.store-switcher .actions.dropdown .dropdown-menu li{margin:0;padding:0}.store-switcher .actions.dropdown .dropdown-menu li:hover{background:0 0;cursor:pointer}.store-switcher .actions.dropdown.active{overflow:visible}.store-switcher .actions.dropdown.active .dropdown-menu{display:block}.store-switcher .actions.dropdown .action.toggle{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;font-weight:400;color:#026294;line-height:normal;margin-top:2px;vertical-align:middle}.store-switcher .actions.dropdown .action.toggle:focus,.store-switcher .actions.dropdown .action.toggle:active{background:0 0;border:none}.store-switcher .actions.dropdown .action.toggle:hover{background:0 0;border:none}.store-switcher .actions.dropdown .action.toggle.disabled,.store-switcher .actions.dropdown .action.toggle[disabled],fieldset[disabled] .store-switcher .actions.dropdown .action.toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.store-switcher .actions.dropdown ul.dropdown-menu{margin-top:4px;padding-top:5px;left:0}.store-switcher .actions.dropdown ul.dropdown-menu li{border:0;cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li:hover{cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li a,.store-switcher .actions.dropdown ul.dropdown-menu li span{padding:5px 13px;display:block;color:#645d53}.store-switcher .actions.dropdown ul.dropdown-menu li a{text-decoration:none}.store-switcher .actions.dropdown ul.dropdown-menu li a:hover{background:#edf9fb}.store-switcher .actions.dropdown ul.dropdown-menu li span{color:#ababab;cursor:default}.store-switcher .actions.dropdown ul.dropdown-menu li.current span{color:#645d53;background:#eee}.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store a,.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store span{padding-left:26px}.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store-view a,.store-switcher .actions.dropdown ul.dropdown-menu .store-switcher-store-view span{padding-left:39px}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar{border-top:1px #ededed solid;margin-top:10px}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar a{text-decoration:none;display:block}.store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar a:before{font-family:'icons-blank-theme';content:'\e606';font-size:20px;line-height:normal;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:text-top;text-align:center;margin:0 3px 0 -4px}.tooltip{display:inline-block;margin-left:5px}.tooltip .help span,.tooltip .help a{width:16px;height:16px;text-align:center;background:rgba(194,186,169,.5);cursor:pointer;border-radius:10px;vertical-align:middle;display:inline-block;text-decoration:none}.tooltip .help span:hover,.tooltip .help a:hover{background:#c2baa9}.tooltip .help span>span,.tooltip .help a>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.tooltip .help span>span.focusable:active,.tooltip .help a>span.focusable:active,.tooltip .help span>span.focusable:focus,.tooltip .help a>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.tooltip .help span>span.focusable:active,.tooltip .help a>span.focusable:active,.tooltip .help span>span.focusable:focus,.tooltip .help a>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.tooltip .help span:before,.tooltip .help a:before{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;content:'?';font-size:13px;line-height:16px;color:#5a534a;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center}.tooltip .help span:before,.tooltip .help a:before{font-weight:700}.tooltip .tooltip-content{display:none;position:absolute;max-width:200px;margin-top:10px;margin-left:-19px;padding:4px 8px;border-radius:3px;background:#000;background:rgba(49,48,43,.8);color:#fff;text-shadow:none;z-index:20}.tooltip .tooltip-content:before{content:'';position:absolute;width:0;height:0;top:-5px;left:20px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000;opacity:.8}.tooltip .tooltip-content.loading{position:absolute}.tooltip .tooltip-content.loading:before{border-bottom-color:rgba(0,0,0,.3)}.tooltip:hover>.tooltip-content{display:block}button,.action-add.mselect-button-add{border-radius:2px;background-image:none;background:#f2ebde;padding:6px 13px;color:#645d53;border:1px solid #ada89e;cursor:pointer;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:0;vertical-align:middle}button:focus,button:active,.action-add.mselect-button-add:focus,.action-add.mselect-button-add:active{background:#cac3b4;border:1px solid #989287}button:hover,.action-add.mselect-button-add:hover{background:#cac3b4}button.disabled,button[disabled],fieldset[disabled] button,.action-add.mselect-button-add.disabled,.action-add.mselect-button-add[disabled],fieldset[disabled] .action-add.mselect-button-add{cursor:default;pointer-events:none;opacity:.5}button.primary,.action-add.mselect-button-add.primary{background-image:none;background:#007dbd;padding:6px 13px;color:#fff;border:1px solid #0a6c9f;cursor:pointer;display:inline-block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;box-sizing:border-box;vertical-align:middle}button.primary:focus,button.primary:active,.action-add.mselect-button-add.primary:focus,.action-add.mselect-button-add.primary:active{background:#026294;border:1px solid #004c74;color:#fff}button.primary:hover,.action-add.mselect-button-add.primary:hover{background:#026294;border:1px solid #026294}button.primary.disabled,button.primary[disabled],fieldset[disabled] button.primary,.action-add.mselect-button-add.primary.disabled,.action-add.mselect-button-add.primary[disabled],fieldset[disabled] .action-add.mselect-button-add.primary{cursor:default;pointer-events:none;opacity:.5}.actions-split{display:inline-block;position:relative;vertical-align:middle}.actions-split button,.actions-split .action-add.mselect-button-add{margin-left:0!important}.actions-split:before,.actions-split:after{content:"";display:table}.actions-split:after{clear:both}.actions-split:before,.actions-split:after{content:"";display:table}.actions-split:after{clear:both}.actions-split .action-default{float:left;margin:0}.actions-split .action-toggle{float:right;margin:0}.actions-split button.action-default,.actions-split .action-add.mselect-button-add.action-default{border-top-right-radius:0;border-bottom-right-radius:0}.actions-split button+.action-toggle,.actions-split .action-add.mselect-button-add+.action-toggle{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.actions-split .action-toggle{padding:6px 5px;display:inline-block;text-decoration:none}.actions-split .action-toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.actions-split .action-toggle>span.focusable:active,.actions-split .action-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle>span.focusable:active,.actions-split .action-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.actions-split .action-toggle:hover:after{color:inherit}.actions-split .action-toggle:active:after{color:inherit}.actions-split .action-toggle.active{display:inline-block;text-decoration:none}.actions-split .action-toggle.active>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.actions-split .action-toggle.active>span.focusable:active,.actions-split .action-toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle.active>span.focusable:active,.actions-split .action-toggle.active>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.actions-split .action-toggle.active:after{font-family:'icons-blank-theme';content:'\e618';font-size:22px;line-height:14px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.actions-split .action-toggle.active:hover:after{color:inherit}.actions-split .action-toggle.active:active:after{color:inherit}.actions-split .dropdown-menu{margin:0;padding:0;list-style:none none;box-sizing:border-box;background:#fff;border:1px solid #bbb;position:absolute;z-index:100;top:100%;min-width:175px;margin-top:4px;display:none;box-shadow:0 3px 3px rgba(0,0,0,.15)}.actions-split .dropdown-menu li{margin:0;padding:3px 5px}.actions-split .dropdown-menu li:hover{background:#e8e8e8;cursor:pointer}.actions-split .dropdown-menu:before,.actions-split .dropdown-menu:after{content:"";position:absolute;display:block;width:0;height:0;border-bottom-style:solid}.actions-split .dropdown-menu:before{z-index:99;border:solid 6px;border-color:transparent transparent #fff}.actions-split .dropdown-menu:after{z-index:98;border:solid 7px;border-color:transparent transparent #bbb}.actions-split .dropdown-menu:before{top:-12px;right:10px}.actions-split .dropdown-menu:after{top:-14px;right:9px}.actions-split.active{overflow:visible}.actions-split.active .dropdown-menu{display:block}.actions-split .action-toggle:after{height:13px}.page-content:after{content:"";display:table;clear:both}.page-wrapper>.page-content{margin-bottom:20px}.page-footer{padding:15px 0}.page-footer-wrapper{background-color:#e0dacf;margin-top:auto}.page-footer:after{content:"";display:table;clear:both}.footer-legal{float:right;width:550px}.footer-legal .link-report,.footer-legal .magento-version,.footer-legal .copyright{font-size:13px}.footer-legal:before{content:"";display:inline-block;vertical-align:middle;position:absolute;z-index:1;margin-top:2px;margin-left:-35px;width:30px;height:35px;background-size:109px 70px;background:url("../images/logo.svg") no-repeat 0 -21px}.icon-error{margin-left:15px;color:#c00815;font-size:11px}.icon-error:before{font-family:'MUI-Icons';content:"\e086";font-size:13px;line-height:13px;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center;margin:-1px 5px 0 0}.ui-widget-overlay{position:fixed}.control .nested{padding:0}.control *:first-child{margin-bottom:0}.field-tooltip{display:inline-block;vertical-align:top;margin-top:5px;position:relative;z-index:1;width:0;overflow:visible}.field-choice .field-tooltip{margin-top:10px}.field-tooltip:hover{z-index:99}.field-tooltip-action{position:relative;z-index:2;margin-left:18px;width:22px;height:22px;display:inline-block;cursor:pointer}.field-tooltip-action:before{content:"?";font-weight:500;font-size:18px;display:inline-block;overflow:hidden;height:22px;border-radius:11px;line-height:22px;width:22px;text-align:center;color:#fff;background-color:#514943}.field-tooltip-action span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.field-tooltip-action span.focusable:active,.field-tooltip-action span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.field-tooltip-action span.focusable:active,.field-tooltip-action span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text:focus+.field-tooltip-content,.field-tooltip:hover .field-tooltip-content{display:block}.field-tooltip-content{display:none;position:absolute;z-index:1;width:320px;background:#fff8d7;padding:15px 25px;right:-66px;border:1px solid #adadad;border-radius:1px;bottom:42px;box-shadow:0 2px 8px 0 rgba(0,0,0,.3)}.field-tooltip-content:after,.field-tooltip-content:before{content:"";display:block;width:0;height:0;border:16px solid transparent;border-top-color:#adadad;position:absolute;right:20px;top:100%;z-index:3}.field-tooltip-content:after{border-top-color:#fff8d7;margin-top:-1px;z-index:4}.form__field.field-error .control [class*=control-]{border-color:#e22626}.form__field.field-error .control [class*=control-]:before{border-color:#e22626}.form__field .mage-error{border:1px solid #e22626;display:block;margin:2px 0 0;padding:6px 10px 10px;background:#fff8d6;color:#555;font-size:12px;font-weight:500;box-sizing:border-box;max-width:380px}.no-flexbox.no-flexboxlegacy .form__field .control-addon+.mage-error{display:inline-block;width:100%}.form__field{position:relative;z-index:1}.form__field:hover{z-index:2}.control .form__field{position:static}.form__field[data-config-scope]:before{content:attr(data-config-scope);display:inline-block;position:absolute;color:gray;right:0;top:6px}.control .form__field[data-config-scope]:nth-child(n+2):before{content:""}.form__field.field-disabled>.label{color:#999}.form__field.field-disabled.field .control [class*=control-][disabled]{background-color:#e9e9e9;opacity:.5;color:#303030;border-color:#adadad}.control-fields .label~.control{width:100%}.form__field{border:0;padding:0}.form__field .note{color:#303030;padding:0;margin:10px 0 0;max-width:380px}.form__field .note:before{display:none}.form__field.form__field{margin-bottom:0}.form__field.form__field+.form__field.form__field{margin-top:15px}.form__field.form__field:not(.choice)~.choice{margin-left:20px;margin-top:5px}.form__field.form__field.choice~.choice{margin-top:9px}.form__field.form__field~.choice:last-child{margin-bottom:8px}.fieldset>.form__field{margin-bottom:30px}.form__field .label{color:#303030}.form__field:not(.choice)>.label{font-size:14px;font-weight:600;width:30%;padding-right:30px;padding-top:0;line-height:33px;white-space:nowrap}.form__field:not(.choice)>.label:before{content:".";visibility:hidden;width:0;margin-left:-7px;overflow:hidden}.form__field:not(.choice)>.label span{white-space:normal;display:inline-block;vertical-align:middle;line-height:1.2}.form__field.required>.label:after{content:"";margin-left:0}.form__field.required>.label span:after{content:"*";color:#eb5202;display:inline;font-weight:500;font-size:16px;margin-top:2px;position:absolute;z-index:1;margin-left:10px}.form__field .control-file{margin-top:6px}.form__field .control-select{line-height:33px}.form__field .control-select:not([multiple]),.form__field .control-text{height:33px;max-width:380px}.form__field .control-addon{max-width:380px}.form__field .control-textarea,.form__field .control-select,.form__field .control-text{border:1px solid #adadad;border-radius:1px;padding:0 10px;color:#303030;background-color:#fff;font-weight:500;font-size:15px;min-width:11em}.form__field .control-textarea:focus,.form__field .control-select:focus,.form__field .control-text:focus{outline:0;border-color:#007bdb;box-shadow:none}.form__field .control-text{line-height:auto}.form__field .control-textarea{padding-top:6px;padding-bottom:6px;line-height:1.18em}.form__field .control-select[multiple],.form__field .control-textarea{width:100%;height:calc(6*1.2em + 14px)}.form__field .control-value{display:inline-block;padding:6px 10px}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:active,.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:active,.form__field .control-fields .form__field:nth-child(n+2):not(.choice)>.label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field .control-select{padding:0}.form__field .control-select option{box-sizing:border-box;padding:4px 10px;display:block}.form__field .control-select optgroup{font-weight:600;display:block;padding:4px 10px;line-height:33px;list-style:inside;font-style:normal}.form__field .control-range>.form__field:nth-child(2):before{content:"\2014";content:":";display:inline-block;margin-left:-25px;float:left;width:20px;line-height:33px;text-align:center}.form__field.choice{position:relative;z-index:1;padding-top:8px;padding-left:26px;padding-right:0}.form__field.choice .label{font-weight:500;padding:0;display:inline;float:none;line-height:18px}.form__field.choice input{position:absolute;top:8px;margin-top:3px!important}.form__field.choice input[disabled]+.label{opacity:.5;cursor:not-allowed}.control>.form__field.choice{max-width:380px}.control>.form__field.choice:nth-child(1):nth-last-child(2),.control>.form__field.choice:nth-child(2):nth-last-child(1){display:inline-block}.control>.form__field.choice:nth-child(1):nth-last-child(2)+.choice,.control>.form__field.choice:nth-child(2):nth-last-child(1)+.choice{margin-left:41px;margin-top:0}.control>.form__field.choice:nth-child(1):nth-last-child(2)+.choice:before,.control>.form__field.choice:nth-child(2):nth-last-child(1)+.choice:before{content:"";position:absolute;display:inline-block;height:20px;top:8px;left:-20px;width:1px;background:#ccc}.form__field.choice .label{cursor:pointer}.form__field.choice .label:before{content:"";position:absolute;z-index:1;border:1px solid #adadad;width:14px;height:14px;top:10px;left:0;border-radius:2px;background:url("../Magento_Ui/images/choice_bkg.png") no-repeat -100% -100%}.form__field.choice input:focus+.label:before{outline:0;border-color:#007bdb}.form__field.choice .control-radio+.label:before{border-radius:8px}.form__field.choice .control-radio:checked+.label:before{background-position:-26px -1px}.form__field.choice .control-checkbox:checked+.label:before{background-position:-1px -1px}.form__field.choice input{opacity:0}.fieldset>.form__field.choice{margin-left:30%}.form__field .control-after,.form__field .control-before{border:0;color:#858585;font-weight:300;font-size:15px;line-height:33px;display:inline-block;height:33px;box-sizing:border-box;padding:0 3px}.no-flexbox.no-flexboxlegacy .form__field .control-before,.no-flexbox.no-flexboxlegacy .form__field .control-addon{float:left;white-space:nowrap}.form__field .control-addon{display:inline-flex;max-width:380px;width:100%;flex-flow:row nowrap;position:relative;z-index:1}.form__field .control-addon>*{position:relative;z-index:1}.form__field .control-addon .control-text[disabled][type],.form__field .control-addon .control-select[disabled][type],.form__field .control-addon .control-select,.form__field .control-addon .control-text{background:transparent!important;border:0;width:auto;vertical-align:top;order:1;flex:1}.form__field .control-addon .control-text[disabled][type]:focus,.form__field .control-addon .control-select[disabled][type]:focus,.form__field .control-addon .control-select:focus,.form__field .control-addon .control-text:focus{box-shadow:none}.form__field .control-addon .control-text[disabled][type]:focus+label:before,.form__field .control-addon .control-select[disabled][type]:focus+label:before,.form__field .control-addon .control-select:focus+label:before,.form__field .control-addon .control-text:focus+label:before{outline:0;border-color:#007bdb}.form__field .control-addon .control-text[disabled][type]+label,.form__field .control-addon .control-select[disabled][type]+label,.form__field .control-addon .control-select+label,.form__field .control-addon .control-text+label{padding-left:10px;position:static!important;z-index:0}.form__field .control-addon .control-text[disabled][type]+label>*,.form__field .control-addon .control-select[disabled][type]+label>*,.form__field .control-addon .control-select+label>*,.form__field .control-addon .control-text+label>*{vertical-align:top;position:relative;z-index:2}.form__field .control-addon .control-text[disabled][type]+label:before,.form__field .control-addon .control-select[disabled][type]+label:before,.form__field .control-addon .control-select+label:before,.form__field .control-addon .control-text+label:before{box-sizing:border-box;border-radius:1px;border:1px solid #adadad;content:"";display:block;position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;background:#fff}.form__field .control-addon .control-text[disabled][type][disabled]+label:before,.form__field .control-addon .control-select[disabled][type][disabled]+label:before,.form__field .control-addon .control-select[disabled]+label:before,.form__field .control-addon .control-text[disabled]+label:before{opacity:.5;background:#e9e9e9}.form__field .control-after{order:3}.form__field .control-after:last-child{padding-right:10px}.form__field .control-before{order:0}.form__field .control-some{display:flex}.form__field [class*=control-grouped]{display:table;width:100%;table-layout:fixed;box-sizing:border-box}.form__field [class*=control-grouped]>.form__field{display:table-cell;width:50%;vertical-align:top}.form__field [class*=control-grouped]>.form__field>.control{width:100%;float:none}.form__field [class*=control-grouped]>.form__field:nth-child(n+2){padding-left:20px}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:active,.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:active,.form__field [class*=control-grouped]>.form__field:nth-child(n+2):not(.choice) .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.form__field [required]{box-shadow:none}fieldset.form__field{position:relative}fieldset.form__field [class*=control-grouped]>.form__field:first-child>.label,fieldset.form__field .control-fields>.form__field:first-child>.label{position:absolute;left:0;top:0;opacity:0;cursor:pointer;width:30%}.control-text+.ui-datepicker-trigger{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;line-height:inherit;font-weight:400;text-decoration:none;margin-left:-40px;display:inline-block}.control-text+.ui-datepicker-trigger img{display:none}.control-text+.ui-datepicker-trigger:focus,.control-text+.ui-datepicker-trigger:active{background:0 0;border:none}.control-text+.ui-datepicker-trigger:hover{background:0 0;border:none}.control-text+.ui-datepicker-trigger.disabled,.control-text+.ui-datepicker-trigger[disabled],fieldset[disabled] .control-text+.ui-datepicker-trigger{cursor:not-allowed;pointer-events:none;opacity:.5}.control-text+.ui-datepicker-trigger>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.control-text+.ui-datepicker-trigger>span.focusable:active,.control-text+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text+.ui-datepicker-trigger>span.focusable:active,.control-text+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.control-text+.ui-datepicker-trigger:after{font-family:'icons-blank-theme';content:'\e612';font-size:38px;line-height:33px;color:#514943;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}[class*=tab-nav-item]:not(ul):active,[class*=tab-nav-item]:not(ul):focus{box-shadow:none;outline:none}.customer-index-edit .col-2-left-layout,.customer-index-edit .col-1-layout{background:#fff}.customer-index-edit{background:#fff}.customer-index-edit .col-2-left-layout{background:#fff}.customer-index-edit .main-col{padding-left:40px}.customer-index-edit .page-main-actions{background:0 0}.tab-nav.block{margin-bottom:40px}.tab-nav.block:first-child{margin-top:16px}.tab-nav.block .block-title{padding:7px 20px}.tab-nav-items{padding:0;border:1px solid #d3d3d3;box-shadow:0 0 4px rgba(50,50,50,.35);margin:0 0 40px;background:#f7f7f7}.tab-nav-item{padding:0;list-style-type:none;border-bottom:1px solid #e0e0e0;position:relative;margin:0 15px;z-index:1}.tab-nav-item:last-child{border-bottom:0}.tab-nav-item.ui-state-active{z-index:2;background:#fff;padding:1px 14px;border:2px solid #eb5202;margin:-1px}.tab-nav-item.ui-state-active .tab-nav-item-link{padding:13px 15px 13px;color:#eb5202}.tab-nav-item.ui-tabs-loading{position:relative;z-index:1}.tab-nav-item.ui-tabs-loading:before{content:"";display:block;position:absolute;z-index:2;background:url("../images/loader-2.gif") no-repeat 50% 50%;background-size:120px;width:20px;height:20px;top:13px;left:-10px}.tab-nav-item.ui-tabs-loading.ui-state-active:before{top:12px;left:4px}.tab-nav-item-link{display:block;padding:15px;color:#666;line-height:1}.tab-nav-item-link:focus,.tab-nav-item-link:active,.tab-nav-item-link:hover{outline:0;color:#eb5202;text-decoration:none}.ui-state-active .tab-nav-item-link{color:#666;font-weight:600}.tab-nav-item-link.changed{font-style:italic}.listing-tiles{overflow:hidden;margin-top:-10px;margin-left:-10px}.listing-tiles .listing-tile{background-color:#f2ebde;display:block;width:238px;height:200px;float:left;border:1px solid #676056;margin-top:10px;margin-left:10px;border-radius:4px;text-align:center}.listing-tiles .listing-tile.disabled{border-color:red}.listing-tiles .listing-tile.enabled{border-color:green}.listing .disabled{color:red}.listing .enabled{color:green}.pager{text-align:left;padding-bottom:10px}.pager:before,.pager:after{content:"";display:table}.pager:after{clear:both}.pager:before,.pager:after{content:"";display:table}.pager:after{clear:both}.pager [data-part=left]{display:inline-block;width:45%;float:left;text-align:left}.pager [data-part=right]{display:inline-block;width:45%;text-align:right;float:right;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.pager .action-next{cursor:pointer}.pager .action-previous{cursor:pointer}.pager{text-align:left}.pager [data-part=left]{display:inline-block;width:45%;text-align:left}.pager [data-part=right]{display:inline-block;width:45%;text-align:right;float:right}.grid .col-title{min-width:90px;text-align:center}.grid-actions [data-part=search]{display:inline-block;margin:0 30px}.grid-actions [data-part=search] input[type=text]{vertical-align:bottom;width:460px}.grid .actions-split .dropdown-menu{right:auto;left:auto;text-align:left;color:#676056;font-weight:400}.grid .actions-split .dropdown-menu:after{right:auto;left:9px}.grid .actions-split .dropdown-menu:before{right:auto;left:10px}.grid .grid-actions{padding:10px 0}.grid .hor-scroll{padding-top:10px}.grid .select-box{display:inline-block;vertical-align:top;margin:-12px -10px -7px;padding:12px 10px 7px;width:100%}.filters-toggle{color:#645d53;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;box-sizing:border-box;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.filters-toggle:after{font-family:'icons-blank-theme';content:'\e607';font-size:30px;line-height:15px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-toggle:hover:after{color:inherit}.filters-toggle:active:after{color:inherit}.filters-toggle:focus,.filters-toggle:active{background:#cac3b4;border:1px solid #989287}.filters-toggle:hover{background:#cac3b4}.filters-toggle.disabled,.filters-toggle[disabled],fieldset[disabled] .filters-toggle{cursor:default;pointer-events:none;opacity:.5}.filters-toggle:focus,.filters-toggle:active{background:0 0;border:none}.filters-toggle:hover{background:0 0;border:none}.filters-toggle.disabled,.filters-toggle[disabled],fieldset[disabled] .filters-toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.filters-toggle:after{margin-top:2px;margin-left:-3px}.filters-toggle.active:after{content:'\e618'}.filters-current{padding:10px 0;display:none}.filters-current.active{display:block}.filters-items{margin:0;padding:0;list-style:none none;display:inline}.filters-item{display:inline-block;margin:0 5px 5px 0;padding:2px 2px 2px 4px;border-radius:3px;background:#f7f3eb}.filters-item .item-label{font-weight:600}.filters-item .item-label:after{content:": "}.filters-item .action-remove{background-image:none;background:#f2ebde;color:#645d53;border:1px solid #ada89e;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;font-weight:500;line-height:1.4rem;box-sizing:border-box;margin:3px;vertical-align:middle;display:inline-block;text-decoration:none;padding:0}.filters-item .action-remove>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters-item .action-remove>span.focusable:active,.filters-item .action-remove>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-item .action-remove>span.focusable:active,.filters-item .action-remove>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-item .action-remove:before{font-family:'icons-blank-theme';content:'\e616';font-size:16px;line-height:16px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-item .action-remove:hover:before{color:inherit}.filters-item .action-remove:active:before{color:inherit}.filters-item .action-remove:focus,.filters-item .action-remove:active{background:#cac3b4;border:1px solid #989287}.filters-item .action-remove:hover{background:#cac3b4}.filters-item .action-remove.disabled,.filters-item .action-remove[disabled],fieldset[disabled] .filters-item .action-remove{cursor:default;pointer-events:none;opacity:.5}.filters-form{position:relative;z-index:1;margin:14px 0;background:#fff;border:1px solid #bbb;box-shadow:0 3px 3px rgba(0,0,0,.15)}.filters-form .action-close{position:absolute;top:3px;right:7px;color:#645d53;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;box-sizing:border-box;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.filters-form .action-close>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters-form .action-close>span.focusable:active,.filters-form .action-close>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-form .action-close>span.focusable:active,.filters-form .action-close>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters-form .action-close:before{font-family:'icons-blank-theme';content:'\e616';font-size:42px;line-height:42px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.filters-form .action-close:hover:before{color:inherit}.filters-form .action-close:active:before{color:inherit}.filters-form .action-close:focus,.filters-form .action-close:active{background:#cac3b4;border:1px solid #989287}.filters-form .action-close:hover{background:#cac3b4}.filters-form .action-close.disabled,.filters-form .action-close[disabled],fieldset[disabled] .filters-form .action-close{cursor:default;pointer-events:none;opacity:.5}.filters-form .action-close:focus,.filters-form .action-close:active{background:0 0;border:none}.filters-form .action-close:hover{background:0 0;border:none}.filters-form .action-close.disabled,.filters-form .action-close[disabled],fieldset[disabled] .filters-form .action-close{cursor:not-allowed;pointer-events:none;opacity:.5}.filters-actions{margin:18px;text-align:right}.filters-fieldset{padding-bottom:0}.filters-fieldset .field{border:0;margin:0 0 20px;box-sizing:border-box;display:inline-block;padding:0 12px 0 0;width:33.33333333%;vertical-align:top}.filters-fieldset .field:before,.filters-fieldset .field:after{content:"";display:table}.filters-fieldset .field:after{clear:both}.filters-fieldset .field:before,.filters-fieldset .field:after{content:"";display:table}.filters-fieldset .field:after{clear:both}.filters-fieldset .field.choice:before,.filters-fieldset .field.no-label:before{box-sizing:border-box;content:" ";height:1px;float:left;padding:6px 15px 0 0;width:35%}.filters-fieldset .field .description{box-sizing:border-box;float:left;padding:6px 15px 0 0;text-align:right;width:35%}.filters-fieldset .field:not(.choice)>.label{box-sizing:border-box;float:left;padding:6px 15px 0 0;text-align:right;width:35%}.filters-fieldset .field:not(.choice)>.control{float:left;width:65%}.filters-fieldset .field:last-child{margin-bottom:0}.filters-fieldset .field+.fieldset{clear:both}.filters-fieldset .field>.label{font-weight:700}.filters-fieldset .field>.label+br{display:none}.filters-fieldset .field .choice input{vertical-align:top}.filters-fieldset .field .fields.group:before,.filters-fieldset .field .fields.group:after{content:"";display:table}.filters-fieldset .field .fields.group:after{clear:both}.filters-fieldset .field .fields.group:before,.filters-fieldset .field .fields.group:after{content:"";display:table}.filters-fieldset .field .fields.group:after{clear:both}.filters-fieldset .field .fields.group .field{box-sizing:border-box;float:left}.filters-fieldset .field .fields.group.group-2 .field{width:50% !important}.filters-fieldset .field .fields.group.group-3 .field{width:33.3% !important}.filters-fieldset .field .fields.group.group-4 .field{width:25% !important}.filters-fieldset .field .fields.group.group-5 .field{width:20% !important}.filters-fieldset .field .addon{display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;padding:0;width:100%}.filters-fieldset .field .addon textarea,.filters-fieldset .field .addon select,.filters-fieldset .field .addon input{-ms-flex-order:2;-webkit-order:2;order:2;-webkit-flex-basis:100%;flex-basis:100%;box-shadow:none;display:inline-block;margin:0;width:auto}.filters-fieldset .field .addon .addbefore,.filters-fieldset .field .addon .addafter{-ms-flex-order:3;-webkit-order:3;order:3;display:inline-block;box-sizing:border-box;background:#fff;border:1px solid #c2c2c2;border-radius:1px;height:32px;padding:0 9px;font-size:14px;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.428571429;background-clip:padding-box;width:auto;white-space:nowrap;vertical-align:middle}.filters-fieldset .field .addon .addbefore:disabled,.filters-fieldset .field .addon .addafter:disabled{opacity:.5}.filters-fieldset .field .addon .addbefore::-moz-placeholder,.filters-fieldset .field .addon .addafter::-moz-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore::-webkit-input-placeholder,.filters-fieldset .field .addon .addafter::-webkit-input-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore:-ms-input-placeholder,.filters-fieldset .field .addon .addafter:-ms-input-placeholder{color:#c2c2c2}.filters-fieldset .field .addon .addbefore{float:left;-ms-flex-order:1;-webkit-order:1;order:1}.filters-fieldset .field .additional{margin-top:10px}.filters-fieldset .field.required>.label:after{content:'*';font-size:1.2rem;color:#e02b27;margin:0 0 0 5px}.filters-fieldset .field .note{font-size:1.2rem;margin:3px 0 0;padding:0;display:inline-block;text-decoration:none}.filters-fieldset .field .note:before{font-family:'icons-blank-theme';content:'\e618';font-size:24px;line-height:12px;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filters-fieldset .field .label{color:#676056;font-size:13px;font-weight:600;margin:0}.filters .field-date .group .hasDatepicker{width:100%;padding-right:30px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger{background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;line-height:inherit;font-weight:400;text-decoration:none;margin-left:-33px;display:inline-block;width:30px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger img{display:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:focus,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:active{background:0 0;border:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:hover{background:0 0;border:none}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger.disabled,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger[disabled],fieldset[disabled] .filters .field-date .group .hasDatepicker+.ui-datepicker-trigger{cursor:not-allowed;pointer-events:none;opacity:.5}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:active,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:active,.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.filters .field-date .group .hasDatepicker+.ui-datepicker-trigger:after{font-family:'icons-blank-theme';content:'\e612';font-size:35px;line-height:30px;color:#514943;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:middle;text-align:center}.filters .field-range .group .field{margin-bottom:0}.filters .field-range .group .control{width:100%;box-sizing:border-box;padding-right:0;position:relative;z-index:1}.mass-select{position:relative;margin:-6px -10px;padding:6px 2px 6px 10px;z-index:1;white-space:nowrap}.mass-select.active{background:rgba(0,0,0,.2)}.mass-select-toggle{color:#645d53;cursor:pointer;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:1.3rem;box-sizing:border-box;vertical-align:middle;display:inline-block;background-image:none;background:0 0;border:0;margin:0;padding:0;-moz-box-sizing:content-box;box-shadow:none;text-shadow:none;text-decoration:none;line-height:inherit;font-weight:400}.mass-select-toggle>span{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.mass-select-toggle>span.focusable:active,.mass-select-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.mass-select-toggle>span.focusable:active,.mass-select-toggle>span.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.mass-select-toggle:before{font-family:'icons-blank-theme';content:'\e607';font-size:30px;line-height:15px;color:inherit;overflow:hidden;speak:none;font-weight:400;-webkit-font-smoothing:antialiased;display:inline-block;vertical-align:top;text-align:center;margin:0}.mass-select-toggle:hover:before{color:inherit}.mass-select-toggle:active:before{color:inherit}.mass-select-toggle:focus,.mass-select-toggle:active{background:#cac3b4;border:1px solid #989287}.mass-select-toggle:hover{background:#cac3b4}.mass-select-toggle.disabled,.mass-select-toggle[disabled],fieldset[disabled] .mass-select-toggle{cursor:default;pointer-events:none;opacity:.5}.mass-select-toggle:focus,.mass-select-toggle:active{background:0 0;border:none}.mass-select-toggle:hover{background:0 0;border:none}.mass-select-toggle.disabled,.mass-select-toggle[disabled],fieldset[disabled] .mass-select-toggle{cursor:not-allowed;pointer-events:none;opacity:.5}.mass-select-toggle:before{margin-top:-2px;text-indent:-5px;color:#fff}.mass-select-toggle:hover:before{color:#fff}.mass-select-toggle:active:before,.mass-select-toggle.active:before{content:'\e618'}.mass-select-field{display:inline}.mass-select-menu{display:none;position:absolute;top:100%;left:0;text-align:left;margin:0;padding:0;list-style:none none;background:#fff;border:1px solid #bbb;min-width:175px;box-shadow:0 3px 3px rgba(0,0,0,.15)}.mass-select-menu li{margin:0;padding:4px 15px;border-bottom:1px solid #e5e5e5}.mass-select-menu li:hover{background:#e8e8e8;cursor:pointer}.mass-select-menu span{font-weight:400;font-size:13px;color:#645d53}.mass-select-menu.active{display:block}.grid-loading-mask{position:absolute;left:0;top:0;right:0;bottom:0;background:rgba(255,255,255,.5);z-index:100}.grid-loading-mask .grid-loader{position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;width:218px;height:149px;background:url('../images/loader-2.gif') 50% 50% no-repeat}.addon input{border-width:1px 0 1px 1px}.addon input~.addafter strong{display:inline-block;background:#fff;line-height:24px;margin:0 3px 0 -2px;padding-left:4px;padding-right:4px;position:relative;font-size:12px;top:0}.addon input:focus~.addafter{border-color:#75b9f0;box-shadow:0 0 8px rgba(82,168,236,.6)}.addon input:focus~.addafter strong{margin-top:0}.addon .addafter{background:0 0;color:#a6a6a6;border-width:1px 1px 1px 0;border-radius:2px 2px 0 0;padding:0;border-color:#ada89e}.addon .pager input{border-width:1px}.field .control input[type=text][disabled],.field .control input[type=text][disabled]~.addafter,.field .control select[disabled],.field .control select[disabled]~.addafter{background-color:#fff;border-color:#eee;box-shadow:none;color:#999}.field .control input[type=text][disabled]~.addafter strong,.field .control select[disabled]~.addafter strong{background-color:#fff}.field-price.addon{direction:rtl}.field-price.addon>*{direction:ltr}.field-price.addon .addafter{border-width:1px 0 1px 1px;border-radius:2px 0 0 2px}.field-price.addon input:first-child{border-radius:0 2px 2px 0}.field-price input{border-width:1px 1px 1px 0}.field-price input:focus{box-shadow:0 0 8px rgba(82,168,236,.6)}.field-price input:focus~label.addafter{box-shadow:0 0 8px rgba(82,168,236,.6)}.field-price input~label.addafter strong{margin-left:2px;margin-right:-2px}.field-price.addon>input{width:99px;float:left}.field-price .control{position:relative}.field-price label.mage-error{position:absolute;left:0;top:30px}.version-fieldset .grid-actions{border-bottom:1px solid #f2ebde;margin:0 0 15px;padding:0 0 15px}.navigation>ul,.message-system,.page-header,.page-actions.fixed .page-actions-inner,.page-content,.page-footer{min-width:960px;max-width:1300px;margin:0 auto;padding-left:15px;padding-right:15px;box-sizing:border-box;width:100%}.pager label.page,.filters .field-range .group .label,.mass-select-field .label{clip:rect(0,0,0,0);border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visually-hidden.focusable:active,.visually-hidden.focusable:focus,.pager label.page.focusable:active,.pager label.page.focusable:focus,.filters .field-range .group .label.focusable:active,.filters .field-range .group .label.focusable:focus,.mass-select-field .label.focusable:active,.mass-select-field .label.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}table th.required:after,.data-table th.required-entry:after,.data-table td.required-entry:after,.grid-actions .filter.required .label span:after,.grid-actions .required:after,.accordion .config .data-table td.required-entry:after{content:'*';color:#e22626;font-weight:400;margin-left:3px}.grid th.required:after,.grid th .required:after{content:'*';color:#f9d4d4;font-weight:400;margin-left:3px}.grid td.col-period,.grid td.col-date,.grid td.col-date_to,.grid td.col-date_from,.grid td.col-ended_at,.grid td.col-created_at,.grid td.col-updated_at,.grid td.col-customer_since,.grid td.col-session_start_time,.grid td.col-last_activity,.grid td.col-email,.grid td.col-name,.grid td.col-sku,.grid td.col-firstname,.grid td.col-lastname,.grid td.col-title,.grid td.col-label,.grid td.col-product,.grid td.col-set_name,.grid td.col-websites,.grid td.col-time,.grid td.col-billing_name,.grid td.col-shipping_name,.grid td.col-phone,.grid td.col-type,.product-options .grouped-items-table .col-name,.product-options .grouped-items-table .col-sku,.sales-order-create-index .data-table .col-product,[class^=' adminhtml-rma-'] .fieldset-wrapper .data-table td,[class^=' adminhtml-rma-'] .grid .col-product_sku,[class^=' adminhtml-rma-'] .grid .col-product_name,.col-grid_segment_name,.adminhtml-catalog-event-index .col-category,[class^=' catalog-search'] .col-search_query,[class^=' catalog-search'] .col-synonym_for,[class^=' catalog-search'] .col-redirect,.adminhtml-urlrewrite-index .col-request_path,.adminhtml-cms-page-index .col-title,.adminhtml-cms-page-index .col-identifier,.adminhtml-cms-hierarchy-index .col-title,.adminhtml-cms-hierarchy-index .col-identifier,.col-banner_name,.adminhtml-widget-instance-index .col-title,.reports-index-search .col-query_text,.adminhtml-rma-item-attribute-index .grid .col-attr-code,.adminhtml-system-store-index .grid td,.catalog-product-attribute-index .col-attr-code,.catalog-product-attribute-index .col-label,.adminhtml-export-index .col-code,.adminhtml-logging-index .grid .col-fullaction,.adminhtml-system-variable-index .grid .col-code,.adminhtml-logging-index .grid .col-info,.dashboard-secondary .dashboard-item tr>td:first-child,.ui-tabs-panel .dashboard-data .col-name,.data-table-td-max .data-table td,[class^=' adminhtml-rma-'] .fieldset-wrapper .accordion .config .data-table td,.data-table-td-max .accordion .config .data-table td,.order-account-information .data-table td,[class^=' adminhtml-rma-'] .rma-request-details .data-table td{overflow:hidden;text-overflow:ellipsis}td.col-period,td.col-date,td.col-date_to,td.col-date_from,td.col-ended_at,td.col-created_at,td.col-updated_at,td.col-customer_since,td.col-session_start_time,td.col-time,td.col-sku,td.col-type,[class^=' adminhtml-rma-'] #rma_items_grid_table .headings th,.adminhtml-process-list .col-action a,.adminhtml-process-list .col-mode{white-space:nowrap}table thead tr th:first-child,table tfoot tr th:first-child,table tfoot tr td:first-child{border-left:0}table thead tr th:last-child,table tfoot tr th:last-child,table tfoot tr td:last-child{border-right:0}.form-inline .grid-actions .label,.form-inline .massaction .label{padding:0;width:auto}.grid .col-action,.grid .col-actions,.grid .col-qty,.grid .col-purchases,.catalog-product-edit .ui-tabs-panel .grid .col-price,.catalog-product-edit .ui-tabs-panel .grid .col-position{width:50px}.grid .col-order-number,.grid .col-real_order_id,.grid .col-invoice-number,.grid .col-increment_id,.grid .col-transaction-id,.grid .col-parent-transaction-id,.grid .col-reference_id,.grid .col-status,.grid .col-price,.grid .col-position,.grid .col-base_grand_total,.grid .col-grand_total,.grid .col-sort_order,.grid .col-carts,.grid .col-priority,.grid .col-severity,.sales-order-create-index .col-in_products,[class^=' reports-'] [class^=col-total],[class^=' reports-'] [class^=col-average],[class^=' reports-'] [class^=col-ref-],[class^=' reports-'] [class^=col-rate],[class^=' reports-'] [class^=col-tax-amount],[class^=' adminhtml-customer-'] .col-required,.adminhtml-rma-item-attribute-index .col-required,[class^=' adminhtml-customer-'] .col-system,.adminhtml-rma-item-attribute-index .col-system,[class^=' adminhtml-customer-'] .col-is_visible,.adminhtml-rma-item-attribute-index .col-is_visible,[class^=' adminhtml-customer-'] .col-sort_order,.adminhtml-rma-item-attribute-index .col-sort_order,.catalog-product-attribute-index [class^=' col-is_'],.catalog-product-attribute-index .col-required,.catalog-product-attribute-index .col-system,.adminhtml-test-index .col-is_listed,[class^=' tests-report-test'] [class^=col-inv-]{width:70px}.grid .col-phone,.sales-order-view .grid .col-period,.sales-order-create-index .col-phone,[class^=' adminhtml-rma-'] .grid .col-product_sku,.adminhtml-rma-edit .col-product,.adminhtml-rma-edit .col-sku,.catalog-product-edit .ui-tabs-panel .grid .col-name,.catalog-product-edit .ui-tabs-panel .grid .col-type,.catalog-product-edit .ui-tabs-panel .grid .col-sku,.customer-index-index .grid .col-customer_since,.customer-index-index .grid .col-billing_country_id,[class^=' customer-index-'] .fieldset-wrapper .grid .col-created_at,[class^=' customer-index-'] .accordion .grid .col-created_at{max-width:70px;width:70px}.sales-order-view .grid .col-name,.sales-order-create-index .data-table .col-product,[class^=' adminhtml-rma-'] .grid .col-name,[class^=' adminhtml-rma-'] .grid .col-product,[class^=' catalog-search'] .col-search_query,[class^=' catalog-search'] .col-synonym_for,[class^=' catalog-search'] .col-redirect,.adminhtml-urlrewrite-index .col-request_path,.reports-report-shopcart-abandoned .grid .col-name,.tax-rule-index .grid .col-title,.adminhtml-rma-item-attribute-index .grid .col-attr-code,.dashboard-secondary .dashboard-item tr>td:first-child{max-width:150px;width:150px}[class^=' sales-order-'] .grid .col-name,.catalog-category-edit .grid .col-name,.adminhtml-catalog-event-index .col-category,.adminhtml-banner-edit .grid .col-name,.reports-report-product-lowstock .grid .col-sku,.newsletter-problem-index .grid .col-name,.newsletter-problem-index .grid .col-subject,.newsletter-problem-index .grid .col-product,.adminhtml-rma-item-attribute-index .grid .col-label,.adminhtml-export-index .col-label,.adminhtml-export-index .col-code,.adminhtml-scheduled-operation-index .grid .col-name,.adminhtml-logging-index .grid .col-fullaction,.test-report-customer-wishlist-wishlist .grid .col-name,.test-report-customer-wishlist-wishlist .grid .col-subject,.test-report-customer-wishlist-wishlist .grid .col-product{max-width:220px;width:220px}.grid .col-period,.grid .col-date,.grid .col-date_to,.grid .col-date_from,.grid .col-ended_at,.grid .col-created_at,.grid .col-updated_at,.grid .col-customer_since,.grid .col-session_start_time,.grid .col-last_activity,.grid .col-email,.grid .col-items_total,.grid .col-firstname,.grid .col-lastname,.grid .col-status-default,.grid .col-websites,.grid .col-time,.grid .col-billing_name,.grid .col-shipping_name,.sales-order-index .grid .col-name,.product-options .grouped-items-table .col-name,.product-options .grouped-items-table .col-sku,[class^=' sales-order-view'] .grid .col-customer_name,[class^=' adminhtml-rma-'] .grid .col-product_name,.catalog-product-index .grid .col-name,.catalog-product-review-index .grid .col-name,.catalog-product-review-index .grid .col-title,.customer-index-edit .ui-tabs-panel .grid .col-name,.review-product-index .grid .col-name,.adminhtml-cms-page-index .col-title,.adminhtml-cms-page-index .col-identifier,.catalog-product-attribute-index .col-attr-code,.catalog-product-attribute-index .col-label,.adminhtml-logging-index .grid .col-info{max-width:110px;width:110px}.grid .col-name,.grid .col-product,.col-banner_name,.adminhtml-widget-instance-index .col-title,[class^=' adminhtml-customer-'] .col-label,.adminhtml-rma-item-attribute-index .col-label,.adminhtml-system-variable-index .grid .col-code,.ui-tabs-panel .dashboard-data .col-name,.adminhtml-test-index .col-label{max-width:370px;width:370px}.col-grid_segment_name,.reports-index-search .col-query_text{max-width:570px;width:570px}[class^=' adminhtml-rma-'] .fieldset-wrapper .data-table td,.reports-report-product-lowstock .grid .col-name,.reports-report-shopcart-product .grid .col-name,.reports-report-review-customer .grid .col-name,[class^=' adminhtml-rma-'] .fieldset-wrapper .accordion .config .data-table td{max-width:670px;width:670px}.reports-report-sales-invoiced .grid .col-period,.reports-report-sales-refunde .grid .col-period,[class^=' tests-report-test'] .grid .col-period{width:auto}.grid .col-select,.grid .col-id,.grid .col-number{width:40px}.sales-order-create-index .grid,.sales-order-create-index .grid-actions,.adminhtml-export-index .grid-actions,.adminhtml-export-index .grid{padding-left:0;padding-right:0}[class^=' adminhtml-rma-'] .col-actions a,[class^=' customer-index-'] .col-action a,.adminhtml-notification-index .col-actions a{display:block;margin:0 0 3px;white-space:nowrap}.data-table-td-max .accordion .config .data-table td,.order-account-information .data-table td,[class^=' adminhtml-rma-'] .rma-request-details .data-table td{max-width:250px;width:250px}.catalog-product-edit .ui-tabs-panel .grid .hor-scroll,.catalog-product-index .grid .hor-scroll,.review-product-index .grid .hor-scroll,.adminhtml-rma-edit .hor-scroll{overflow-x:auto}.add-clearer:after,.massaction:after,.navigation>ul:after{content:"";display:table;clear:both}.test-content{width:calc(20px + 100*0.2)}.test-content:before{content:'.test {\A ' attr(data-attribute) ': 0.2em;' '\A content:\'';white-space:pre}.test-content:after{content:' Test\';\A}' "\A" '\A.test + .test._other ~ ul > li' " {\A height: @var;\A content: ' + ';\A}";white-space:pre}.test-content-calc{width:calc((100%/12*2) - 10px)}.test-svg-xml-image{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 38 40"><style>.st0{fill:none;stroke:%23ffffff;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}</style><circle cx="14.7" cy="14.7" r="13.7" class="st0"/><path d="M23.9 24.7L37 39" class="st0"/></svg>') no-repeat left center} \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/css/styles.css b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/css/styles.css index 90ca42321c921..35305fb2392d0 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/css/styles.css +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/web/css/styles.css @@ -484,7 +484,6 @@ table .col-draggable .draggable-handle { border: 0; display: inline; line-height: 1.42857143; - margin: 0; padding: 0; color: #1979c3; text-decoration: none; @@ -621,7 +620,6 @@ fieldset[disabled] .grid-actions .action-reset { background: none; border: 0; display: inline; - line-height: 1.42857143; margin: 0; padding: 0; color: #1979c3; @@ -683,7 +681,6 @@ fieldset[disabled] .pager .action-next { text-decoration: none; } .pager .action-previous > span { - clip: rect(0, 0, 0, 0); border: 0; clip: rect(0 0 0 0); height: 1px; @@ -733,7 +730,6 @@ fieldset[disabled] .pager .action-next { text-decoration: none; } .pager .action-next > span { - clip: rect(0, 0, 0, 0); border: 0; clip: rect(0 0 0 0); height: 1px; @@ -1510,7 +1506,6 @@ fieldset[disabled] .pager .action-next { text-decoration: none; } .search-global-field .label > span { - clip: rect(0, 0, 0, 0); border: 0; clip: rect(0 0 0 0); height: 1px; @@ -1622,7 +1617,6 @@ fieldset[disabled] .pager .action-next { color: #676056; } .notifications-action .text { - clip: rect(0, 0, 0, 0); border: 0; clip: rect(0 0 0 0); height: 1px; @@ -1707,7 +1701,6 @@ fieldset[disabled] .pager .action-next { z-index: 1; top: 12px; right: 12px; - display: inline-block; background-image: none; background: none; border: 0; @@ -2358,7 +2351,6 @@ fieldset[disabled] .notifications-close.action { text-decoration: none; } #product-variations-matrix .actions-image-uploader { - display: inline-block; position: relative; display: block; width: 50px; @@ -3109,7 +3101,6 @@ body > * { background-image: none; background: none; border: 0; - margin: 0; padding: 0; -moz-box-sizing: content-box; box-shadow: none; @@ -3396,7 +3387,6 @@ fieldset[disabled] .page-main-actions .page-actions .action-add.mselect-button-a box-shadow: none; text-shadow: none; text-decoration: none; - line-height: inherit; font-weight: 400; color: #026294; line-height: normal; @@ -3464,7 +3454,6 @@ fieldset[disabled] .store-switcher .actions.dropdown .action.toggle { margin-top: 10px; } .store-switcher .actions.dropdown ul.dropdown-menu .dropdown-toolbar a { - display: inline-block; text-decoration: none; display: block; } @@ -4833,17 +4822,11 @@ fieldset[disabled] .control-text + .ui-datepicker-trigger { width: 100%; } .filters-toggle { - background: #f2ebde; - padding: 6px 13px; color: #645d53; - border: 1px solid #ada89e; cursor: pointer; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.3rem; - font-weight: 500; - line-height: 1.4rem; box-sizing: border-box; - margin: 3px; vertical-align: middle; display: inline-block; background-image: none; @@ -4946,7 +4929,6 @@ fieldset[disabled] .filters-toggle { .filters-item .action-remove { background-image: none; background: #f2ebde; - padding: 6px 13px; color: #645d53; border: 1px solid #ada89e; cursor: pointer; @@ -5038,17 +5020,11 @@ fieldset[disabled] .filters-item .action-remove { position: absolute; top: 3px; right: 7px; - background: #f2ebde; - padding: 6px 13px; color: #645d53; - border: 1px solid #ada89e; cursor: pointer; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.3rem; - font-weight: 500; - line-height: 1.4rem; box-sizing: border-box; - margin: 3px; vertical-align: middle; display: inline-block; background-image: none; @@ -5283,13 +5259,11 @@ fieldset[disabled] .filters-form .action-close { border: 1px solid #c2c2c2; border-radius: 1px; height: 32px; - width: 100%; padding: 0 9px; font-size: 14px; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.428571429; background-clip: padding-box; - vertical-align: baseline; width: auto; white-space: nowrap; vertical-align: middle; @@ -5487,17 +5461,11 @@ fieldset[disabled] .filters .field-date .group .hasDatepicker + .ui-datepicker-t background: rgba(0, 0, 0, 0.2); } .mass-select-toggle { - background: #f2ebde; - padding: 6px 13px; color: #645d53; - border: 1px solid #ada89e; cursor: pointer; font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.3rem; - font-weight: 500; - line-height: 1.4rem; box-sizing: border-box; - margin: 3px; vertical-align: middle; display: inline-block; background-image: none; @@ -5754,7 +5722,6 @@ fieldset[disabled] .mass-select-toggle { .page-actions.fixed .page-actions-inner, .page-content, .page-footer { - width: auto; min-width: 960px; max-width: 1300px; margin: 0 auto; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php new file mode 100644 index 0000000000000..aca41b236c7bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/calculation/discount_tax', '1'); +$configWriter->save('tax/display/type', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$configWriter->save('tax/cart_display/price', '3'); +$configWriter->save('tax/cart_display/subtotal', '3'); +$configWriter->save('tax/cart_display/shipping', '3'); +$configWriter->save('tax/cart_display/grandtotal', '1'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings_rollback.php new file mode 100644 index 0000000000000..7448a71dcbf18 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/calculation/discount_tax', '0'); +$configWriter->save('tax/display/type', '1'); +$configWriter->save('tax/display/shipping', '1'); + +$configWriter->save('tax/cart_display/price', '1'); +$configWriter->save('tax/cart_display/subtotal', '1'); +$configWriter->save('tax/cart_display/shipping', '1'); +$configWriter->save('tax/cart_display/grandtotal', '0'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php index f25144c308c68..862a924f65793 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php @@ -7,9 +7,15 @@ namespace Magento\GraphQlCache\Controller; -use PHPUnit\Framework\TestCase; -use Magento\TestFramework\ObjectManager; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; +use Magento\Framework\Registry; +use Magento\GraphQl\Controller\GraphQl as GraphQlController; +use Magento\GraphQlCache\Model\CacheableQuery; +use Magento\PageCache\Model\Cache\Type as PageCache; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; /** * Abstract test class for Graphql cache tests @@ -21,40 +27,114 @@ abstract class AbstractGraphqlCacheTest extends TestCase */ protected $objectManager; - /** - * @inheritdoc - */ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); + $this->enablePageCachePlugin(); + $this->enableCachebleQueryTestProxy(); + } + + protected function tearDown(): void + { + $this->disableCacheableQueryTestProxy(); + $this->disablePageCachePlugin(); + $this->flushPageCache(); + } + + protected function enablePageCachePlugin(): void + { + /** @var $registry Registry */ + $registry = $this->objectManager->get(Registry::class); + $registry->register('use_page_cache_plugin', true, true); + } + + protected function disablePageCachePlugin(): void + { + /** @var $registry Registry */ + $registry = $this->objectManager->get(Registry::class); + $registry->unregister('use_page_cache_plugin'); + } + + protected function flushPageCache(): void + { + /** @var PageCache $fullPageCache */ + $fullPageCache = $this->objectManager->get(PageCache::class); + $fullPageCache->clean(); } /** - * Prepare a query and return a request to be used in the same test end to end + * Regarding the SuppressWarnings annotation below: the nested class below triggers a false rule match. * - * @param string $query - * @return \Magento\Framework\App\Request\Http + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - protected function prepareRequest(string $query) : \Magento\Framework\App\Request\Http - { - $cacheableQuery = $this->objectManager->get(\Magento\GraphQlCache\Model\CacheableQuery::class); - $cacheableQueryReflection = new \ReflectionProperty( - $cacheableQuery, - 'cacheTags' - ); - $cacheableQueryReflection->setAccessible(true); - $cacheableQueryReflection->setValue($cacheableQuery, []); - - /** @var \Magento\Framework\UrlInterface $urlInterface */ - $urlInterface = $this->objectManager->create(\Magento\Framework\UrlInterface::class); - //set unique URL - $urlInterface->setQueryParam('query', $query); - - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); - $request->setUri($urlInterface->getUrl('graphql')); + private function enableCachebleQueryTestProxy(): void + { + $cacheableQueryProxy = new class($this->objectManager) extends CacheableQuery { + /** @var CacheableQuery */ + private $delegate; + + public function __construct(ObjectManager $objectManager) + { + $this->reset($objectManager); + } + + public function reset(ObjectManager $objectManager): void + { + $this->delegate = $objectManager->create(CacheableQuery::class); + } + + public function getCacheTags(): array + { + return $this->delegate->getCacheTags(); + } + + public function addCacheTags(array $cacheTags): void + { + $this->delegate->addCacheTags($cacheTags); + } + + public function isCacheable(): bool + { + return $this->delegate->isCacheable(); + } + + public function setCacheValidity(bool $cacheable): void + { + $this->delegate->setCacheValidity($cacheable); + } + + public function shouldPopulateCacheHeadersWithTags(): bool + { + return $this->delegate->shouldPopulateCacheHeadersWithTags(); + } + }; + $this->objectManager->addSharedInstance($cacheableQueryProxy, CacheableQuery::class); + } + + private function disableCacheableQueryTestProxy(): void + { + $this->resetQueryCacheTags(); + $this->objectManager->removeSharedInstance(CacheableQuery::class); + } + + protected function resetQueryCacheTags(): void + { + $this->objectManager->get(CacheableQuery::class)->reset($this->objectManager); + } + + protected function dispatchGraphQlGETRequest(array $queryParams): HttpResponse + { + $this->resetQueryCacheTags(); + + /** @var HttpRequest $request */ + $request = $this->objectManager->get(HttpRequest::class); + $request->setPathInfo('/graphql'); $request->setMethod('GET'); - //set the actual GET query - $request->setQueryValue('query', $query); - return $request; + $request->setParams($queryParams); + + // required for \Magento\Framework\App\PageCache\Identifier to generate the correct cache key + $request->setUri(implode('?', [$request->getPathInfo(), http_build_query($queryParams)])); + + return $this->objectManager->create(GraphQlController::class)->dispatch($request); } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php index fd97399992c1c..287353bbd2b80 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php @@ -9,8 +9,6 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Request\Http; -use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** @@ -22,31 +20,12 @@ */ class CategoriesWithProductsCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; - - /** - * @var Http - */ - private $request; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->create(Http::class); - } /** * Test cache tags and debug header for category with products querying for products and category * * @magentoDataFixture Magento/Catalog/_files/category_product.php */ - public function testToCheckRequestCacheTagsForCategoryWithProducts(): void + public function testRequestCacheTagsForCategoryWithProducts(): void { /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); @@ -91,17 +70,7 @@ public function testToCheckRequestCacheTagsForCategoryWithProducts(): void 'operationName' => 'GetCategoryWithProducts' ]; - /** @var \Magento\Framework\UrlInterface $urlInterface */ - $urlInterface = $this->objectManager->create(\Magento\Framework\UrlInterface::class); - //set unique URL - $urlInterface->setQueryParam('query', $queryParams['query']); - $urlInterface->setQueryParam('variables', $queryParams['variables']); - $urlInterface->setQueryParam('operationName', $queryParams['operationName']); - $this->request->setUri($urlInterface->getUrl('graphql')); - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setParams($queryParams); - $response = $this->graphqlController->dispatch($this->request); + $response = $this->dispatchGraphQlGETRequest($queryParams); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $expectedCacheTags = ['cat_c','cat_c_' . $categoryId,'cat_p','cat_p_' . $product->getId(),'FPC']; $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php index be920fb200ff3..90bdc4f75825a 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php @@ -7,8 +7,6 @@ namespace Magento\GraphQlCache\Controller\Catalog; -use Magento\Framework\App\Request\Http; -use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** @@ -20,25 +18,12 @@ */ class CategoryCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - } /** * Test cache tags and debug header for category and querying only for category * * @magentoDataFixture Magento/Catalog/_files/category_product.php */ - public function testToCheckRequestCacheTagsForForCategory(): void + public function testRequestCacheTagsForCategory(): void { $categoryId ='333'; $query @@ -53,11 +38,10 @@ public function testToCheckRequestCacheTagsForForCategory(): void } } QUERY; - $request = $this->prepareRequest($query); - $response = $this->graphqlController->dispatch($request); + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId,'FPC']; + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; $this->assertEquals($expectedCacheTags, $actualCacheTags); } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php new file mode 100644 index 0000000000000..a8e9059a84eb6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching works for categoryList query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoryListCacheTest extends AbstractGraphqlCacheTest +{ + /** + * Test cache tags are generated + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testRequestCacheTagsForCategoryList(): void + { + $categoryId ='333'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test request is served from cache + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testSecondRequestIsServedFromCache() + { + $categoryId ='333'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test cache tags are generated + * + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + */ + public function testRequestCacheTagsForCategoryListOnMultipleIds(): void + { + $categoryId1 ='400'; + $categoryId2 = '401'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + //added the previous category in expected tags as it is cached + $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test request is served from cache + * + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + */ + public function testSecondRequestIsServedFromCacheOnMultipleIds() + { + $categoryId1 ='400'; + $categoryId2 = '401'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + //added the previous category in expected tags as it is cached + $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php index 746b37a88770a..6228feae37c15 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php @@ -9,7 +9,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Framework\App\Request\Http; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** @@ -20,24 +19,11 @@ */ class DeepNestedCategoriesAndProductsTest extends AbstractGraphqlCacheTest { - /** @var \Magento\GraphQl\Controller\GraphQl */ - private $graphql; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - parent::setUp(); - $this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - } - /** * Test cache tags and debug header for deep nested queries involving category and products * * @magentoCache all enabled * @magentoDataFixture Magento/Catalog/_files/product_in_multiple_categories.php - * */ public function testDispatchForCacheHeadersOnDeepNestedQueries(): void { @@ -83,15 +69,18 @@ public function testDispatchForCacheHeadersOnDeepNestedQueries(): void $productIdsFromCategory = $category->getProductCollection()->getAllIds(); foreach ($productIdsFromCategory as $productId) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $resolvedCategoryIds = array_merge( $resolvedCategoryIds, $productRepository->getById($productId)->getCategoryIds() ); } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $resolvedCategoryIds = array_merge($resolvedCategoryIds, [$baseCategoryId]); foreach ($resolvedCategoryIds as $categoryId) { $category = $categoryRepository->get($categoryId); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $productIdsFromCategory= array_merge( $productIdsFromCategory, $category->getProductCollection()->getAllIds() @@ -102,14 +91,15 @@ public function testDispatchForCacheHeadersOnDeepNestedQueries(): void $uniqueCategoryIds = array_unique($resolvedCategoryIds); $expectedCacheTags = ['cat_c', 'cat_p', 'FPC']; foreach ($uniqueProductIds as $uniqueProductId) { - $expectedCacheTags = array_merge($expectedCacheTags, ['cat_p_'.$uniqueProductId]); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $expectedCacheTags = array_merge($expectedCacheTags, ['cat_p_' . $uniqueProductId]); } foreach ($uniqueCategoryIds as $uniqueCategoryId) { - $expectedCacheTags = array_merge($expectedCacheTags, ['cat_c_'.$uniqueCategoryId]); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $expectedCacheTags = array_merge($expectedCacheTags, ['cat_c_' . $uniqueCategoryId]); } - $request = $this->prepareRequest($query); - $response = $this->graphql->dispatch($request); + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $this->assertEmpty( diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php index 335067f8408df..038a8c7255815 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php @@ -8,7 +8,6 @@ namespace Magento\GraphQlCache\Controller\Catalog; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** @@ -20,26 +19,12 @@ */ class ProductsCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - } - /** * Test request is dispatched and response is checked for debug headers and cache tags * * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php */ - public function testToCheckRequestCacheTagsForProducts(): void + public function testRequestCacheTagsForProducts(): void { /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); @@ -63,8 +48,7 @@ public function testToCheckRequestCacheTagsForProducts(): void } QUERY; - $request = $this->prepareRequest($query); - $response = $this->graphqlController->dispatch($request); + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; @@ -74,7 +58,7 @@ public function testToCheckRequestCacheTagsForProducts(): void /** * Test request is checked for debug headers and no cache tags for not existing product */ - public function testToCheckRequestNoTagsForProducts(): void + public function testRequestNoTagsForNonExistingProducts(): void { $query = <<<QUERY @@ -93,11 +77,66 @@ public function testToCheckRequestNoTagsForProducts(): void } QUERY; - $request = $this->prepareRequest($query); - $response = $this->graphqlController->dispatch($request); + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $expectedCacheTags = ['FPC']; $this->assertEquals($expectedCacheTags, $actualCacheTags); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + */ + public function testConsecutiveRequestsAreServedFromThePageCache(): void + { + $query + = <<<QUERY +{ + products(filter: {sku: {eq: "simple1"}}) + { + items { + id + name + sku + description { + html + } + } + } +} +QUERY; + $response1 = $this->dispatchGraphQlGETRequest(['query' => $query]); + $response2 = $this->dispatchGraphQlGETRequest(['query' => $query]); + + $this->assertEquals('MISS', $response1->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $this->assertEquals('HIT', $response2->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + */ + public function testDifferentProductsRequestsUseDifferentPageCacheRecords(): void + { + $queryTemplate + = <<<QUERY +{ + products(filter: {sku: {eq: "%s"}}) + { + items { + id + name + sku + description { + html + } + } + } +} +QUERY; + $responseProduct1 = $this->dispatchGraphQlGETRequest(['query' => sprintf($queryTemplate, 'simple1')]); + $responseProduct2 = $this->dispatchGraphQlGETRequest(['query' => sprintf($queryTemplate, 'simple2')]); + + $this->assertEquals('MISS', $responseProduct1->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $this->assertEquals('MISS', $responseProduct2->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php index c9dca2a5a8372..bcc7c623eb18a 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQlCache\Controller\Cms; +use Magento\Cms\Api\Data\BlockInterface; use Magento\Cms\Model\BlockRepository; -use Magento\GraphQl\Controller\GraphQl; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** @@ -20,23 +21,27 @@ */ class BlockCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; + private function assertPageCacheMissWithTagsForCmsBlock(HttpResponse $response, BlockInterface $block): void + { + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $this->assertCmsBlockCacheTags($response, $block); + } - /** - * @inheritdoc - */ - protected function setUp(): void + private function assertPageCacheHitWithTagsForCmsBlock(HttpResponse $response, BlockInterface $block): void + { + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $this->assertCmsBlockCacheTags($response, $block); + } + + private function assertCmsBlockCacheTags(HttpResponse $response, BlockInterface $block): void { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block->getId(), 'cms_b_' . $block->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); } /** - * Test that the correct cache tags get added to request for cmsBlocks - * * @magentoDataFixture Magento/Cms/_files/block.php * @magentoDataFixture Magento/Cms/_files/blocks.php */ @@ -46,9 +51,9 @@ public function testCmsBlocksRequestHasCorrectTags(): void $blockRepository = $this->objectManager->get(BlockRepository::class); $block1Identifier = 'fixture_block'; - $block1 = $blockRepository->getById($block1Identifier); + $block1 = $blockRepository->getById($block1Identifier); $block2Identifier = 'enabled_block'; - $block2 = $blockRepository->getById($block2Identifier); + $block2 = $blockRepository->getById($block2Identifier); $queryBlock1 = <<<QUERY @@ -77,60 +82,30 @@ public function testCmsBlocksRequestHasCorrectTags(): void QUERY; // check to see that the first entity gets a MISS when called the first time - $request = $this->prepareRequest($queryBlock1); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock1]); + $this->assertPageCacheMissWithTagsForCmsBlock($response, $block1); - // check to see that the second entity gets a miss when called the first time - $request = $this->prepareRequest($queryBlock2); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block2->getId(), 'cms_b_' . $block2->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + // check to see that the second entity gets a MISS when called the first time + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock2]); + $this->assertPageCacheMissWithTagsForCmsBlock($response, $block2); // check to see that the first entity gets a HIT when called the second time - $request = $this->prepareRequest($queryBlock1); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock1]); + $this->assertPageCacheHitWithTagsForCmsBlock($response, $block1); // check to see that the second entity gets a HIT when called the second time - $request = $this->prepareRequest($queryBlock2); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block2->getId(), 'cms_b_' . $block2->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock2]); + $this->assertPageCacheHitWithTagsForCmsBlock($response, $block2); $block1->setTitle('something else that causes invalidation'); $blockRepository->save($block1); // check to see that the first entity gets a MISS and it was invalidated - $request = $this->prepareRequest($queryBlock1); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock1]); + $this->assertPageCacheMissWithTagsForCmsBlock($response, $block1); // check to see that the first entity gets a HIT when called the second time - $request = $this->prepareRequest($queryBlock1); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $queryBlock1]); + $this->assertPageCacheHitWithTagsForCmsBlock($response, $block1); } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php index 0248f870a5f11..60d84947d87c8 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php @@ -7,13 +7,14 @@ namespace Magento\GraphQlCache\Controller\Cms; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Model\GetPageByIdentifier; use Magento\Cms\Model\PageRepository; -use Magento\GraphQl\Controller\GraphQl; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; /** - * Test caching works for CMS page + * Test caching works for CMS pages * * @magentoAppArea graphql * @magentoCache full_page enabled @@ -22,123 +23,34 @@ */ class CmsPageCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - } - - /** - * Test that the correct cache tags get added to request for cmsPage query - * - * @magentoDataFixture Magento/Cms/_files/pages.php - */ - public function testToCheckCmsPageRequestCacheTags(): void + private function assertPageCacheMissWithTagsForCmsPage(string $pageId, string $name, HttpResponse $response): void { - $cmsPage100 = $this->objectManager->get(GetPageByIdentifier::class)->execute('page100', 0); - $pageId100 = $cmsPage100->getId(); - - $cmsPageBlank = $this->objectManager->get(GetPageByIdentifier::class)->execute('page_design_blank', 0); - $pageIdBlank = $cmsPageBlank->getId(); - - $queryCmsPage100 = $this->getQuery($pageId100); - $queryCmsPageBlank = $this->getQuery($pageIdBlank); - - // check to see that the first entity gets a MISS when called the first time - $request = $this->prepareRequest($queryCmsPage100); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals( - 'MISS', - $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected MISS on page page100 id {$queryCmsPage100}" - ); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); - - // check to see that the second entity gets a miss when called the first time - $request = $this->prepareRequest($queryCmsPageBlank); - $response = $this->graphqlController->dispatch($request); $this->assertEquals( 'MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected MISS on page pageBlank id {$pageIdBlank}" - ); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageIdBlank , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); - - // check to see that the first entity gets a HIT when called the second time - $request = $this->prepareRequest($queryCmsPage100); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals( - 'HIT', - $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected HIT on page page100 id {$queryCmsPage100}" - ); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); - - // check to see that the second entity gets a HIT when called the second time - $request = $this->prepareRequest($queryCmsPageBlank); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals( - 'HIT', - $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected HIT on page pageBlank id {$pageIdBlank}" + "expected MISS on page {$name} id {$pageId}" ); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageIdBlank , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); - - /** @var PageRepository $pageRepository */ - $pageRepository = $this->objectManager->get(PageRepository::class); - - $page = $pageRepository->getById($pageId100); - $page->setTitle('something else that causes invalidation'); - $pageRepository->save($page); - - // check to see that the first entity gets a MISS and it was invalidated - $request = $this->prepareRequest($queryCmsPage100); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals( - 'MISS', - $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected MISS on page page100 id {$queryCmsPage100}" - ); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); + $this->assertCmsPageCacheTags($pageId, $response); + } - // check to see that the first entity gets a HIT when called the second time - $request = $this->prepareRequest($queryCmsPage100); - $response = $this->graphqlController->dispatch($request); + private function assertPageCacheHitWithTagsForCmsPage(string $pageId, string $name, HttpResponse $response): void + { $this->assertEquals( 'HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), - "expected MISS on page page100 id {$queryCmsPage100}" + "expected HIT on page {$name} id {$pageId}" ); + $this->assertCmsPageCacheTags($pageId, $response); + } + + private function assertCmsPageCacheTags(string $pageId, HttpResponse $response): void + { $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; + $expectedCacheTags = ['cms_p', 'cms_p_' . $pageId, 'FPC']; $this->assertEquals($expectedCacheTags, $requestedCacheTags); } - /** - * Get cms query - * - * @param string $id - * @return string - */ - private function getQuery(string $id) : string + private function buildQuery(string $id): string { $queryCmsPage = <<<QUERY { @@ -156,4 +68,57 @@ private function getQuery(string $id) : string QUERY; return $queryCmsPage; } + + private function updateCmsPageTitle(string $pageId100, string $newTitle): void + { + /** @var PageRepository $pageRepository */ + $pageRepository = $this->objectManager->get(PageRepository::class); + $page = $pageRepository->getById($pageId100); + $page->setTitle($newTitle); + $pageRepository->save($page); + } + + /** + * @magentoDataFixture Magento/Cms/_files/pages.php + */ + public function testCmsPageRequestCacheTags(): void + { + /** @var PageInterface $cmsPage100 */ + $cmsPage100 = $this->objectManager->get(GetPageByIdentifier::class)->execute('page100', 0); + $pageId100 = (string) $cmsPage100->getId(); + + /** @var PageInterface $cmsPageBlank */ + $cmsPageBlank = $this->objectManager->get(GetPageByIdentifier::class)->execute('page_design_blank', 0); + $pageIdBlank = (string) $cmsPageBlank->getId(); + + $queryCmsPage100 = $this->buildQuery($pageId100); + $queryCmsPageBlank = $this->buildQuery($pageIdBlank); + + // check to see that the first entity gets a MISS when called the first time + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPage100]); + $this->assertPageCacheMissWithTagsForCmsPage($pageId100, 'page100', $response); + + // check to see that the second entity gets a MISS when called the first time + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPageBlank]); + $this->assertPageCacheMissWithTagsForCmsPage($pageIdBlank, 'pageBlank', $response); + + // check to see that the first entity gets a HIT when called the second time + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPage100]); + $this->assertPageCacheHitWithTagsForCmsPage($pageId100, 'page100', $response); + + // check to see that the second entity gets a HIT when called the second time + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPageBlank]); + $this->assertPageCacheHitWithTagsForCmsPage($pageIdBlank, 'pageBlank', $response); + + // invalidate first entity + $this->updateCmsPageTitle($pageId100, 'something else that causes invalidation'); + + // check to see that the second entity gets a HIT to confirm only the first was invalidated + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPageBlank]); + $this->assertPageCacheHitWithTagsForCmsPage($pageIdBlank, 'pageBlank', $response); + + // check to see that the first entity gets a MISS because it was invalidated + $response = $this->dispatchGraphQlGETRequest(['query' => $queryCmsPage100]); + $this->assertPageCacheMissWithTagsForCmsPage($pageId100, 'page100', $response); + } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php index 7accb1d7d0b26..81a4988b81935 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php @@ -9,7 +9,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; -use Magento\GraphQl\Controller\GraphQl; +use Magento\Framework\App\Response\HttpInterface as HttpResponse; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\Cms\Api\Data\PageInterface; @@ -24,155 +24,153 @@ */ class AllEntitiesUrlResolverCacheTest extends AbstractGraphqlCacheTest { - /** - * @var GraphQl - */ - private $graphqlController; + private function assertCacheMISSWithTagsForCategory(string $categoryId, HttpResponse $response): void + { + $this->assertCacheMISS($response); + $this->assertCacheTags($categoryId, 'cat_c', $response); + } - /** - * @inheritdoc - */ - protected function setUp(): void + private function assertCacheMISSWithTagsForProduct(string $productId, HttpResponse $response): void + { + $this->assertCacheMISS($response); + $this->assertCacheTags($productId, 'cat_p', $response); + } + + private function assertCacheMISSWithTagsForCmsPage(string $pageId, HttpResponse $response): void + { + $this->assertCacheMISS($response); + $this->assertCacheTags($pageId, 'cms_p', $response); + } + + private function assertCacheHITWithTagsForCategory(string $categoryId, HttpResponse $response): void { - parent::setUp(); - $this->graphqlController = $this->objectManager->get(GraphQl::class); + $this->assertCacheHIT($response); + $this->assertCacheTags($categoryId, 'cat_c', $response); + } + + private function assertCacheHITWithTagsForProduct(string $productId, HttpResponse $response): void + { + $this->assertCacheHIT($response); + $this->assertCacheTags($productId, 'cat_p', $response); + } + + private function assertCacheHITWithTagsForCmsPage(string $pageId, HttpResponse $response): void + { + $this->assertCacheHIT($response); + $this->assertCacheTags($pageId, 'cms_p', $response); + } + + private function assertCacheMISS(HttpResponse $response): void + { + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + } + + private function assertCacheHIT(HttpResponse $response): void + { + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + } + + private function assertCacheTags(string $entityId, string $entityCacheTag, HttpResponse $response) + { + $expectedCacheTags = [$entityCacheTag, $entityCacheTag . '_' . $entityId, 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + private function buildQuery(string $requestPath): string + { + $resolverQuery = <<<QUERY +{ + urlResolver(url:"{$requestPath}") + { + id + relative_url + canonical_url + type + } +} +QUERY; + return $resolverQuery; } /** - * Tests that X-Magento-tags and cache debug headers are correct for category urlResolver + * Tests that X-Magento-Tags and cache debug headers are correct for category urlResolver * * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php * @magentoDataFixture Magento/Cms/_files/pages.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testAllEntitiesUrlResolverRequestHasCorrectTags() + public function testAllEntitiesUrlResolverRequestHasCorrectTags(): void { $categoryUrlKey = 'cat-1.html'; - $productUrlKey = 'p002.html'; - $productSku = 'p002'; + $productUrlKey = 'p002.html'; + $productSku = 'p002'; /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); /** @var Product $product */ $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); + $storeId = (string) $product->getStoreId(); /** @var UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( [ 'request_path' => $categoryUrlKey, 'store_id' => $storeId ] ); - $categoryId = $actualUrls->getEntityId(); - $categoryQuery = $this->getQuery($categoryUrlKey); + $categoryId = (string) $actualUrls->getEntityId(); + $categoryQuery = $this->buildQuery($categoryUrlKey); - $productQuery = $this->getQuery($productUrlKey); + $productQuery = $this->buildQuery($productUrlKey); /** @var GetPageByIdentifierInterface $page */ $page = $this->objectManager->get(GetPageByIdentifierInterface::class); /** @var PageInterface $cmsPage */ - $cmsPage = $page->execute('page100', 0); - $cmsPageId = $cmsPage->getId(); + $cmsPage = $page->execute('page100', 0); + $cmsPageId = (string) $cmsPage->getId(); $requestPath = $cmsPage->getIdentifier(); - $pageQuery = $this->getQuery($requestPath); + $pageQuery = $this->buildQuery($requestPath); // query category for MISS - $request = $this->prepareRequest($categoryQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $categoryQuery]); + $this->assertCacheMISSWithTagsForCategory($categoryId, $response); // query product for MISS - $request = $this->prepareRequest($productQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $productQuery]); + $this->assertCacheMISSWithTagsForProduct((string) $product->getId(), $response); // query page for MISS - $request = $this->prepareRequest($pageQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_p','cms_p_' . $cmsPageId,'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $pageQuery]); + $this->assertCacheMISSWithTagsForCmsPage($cmsPageId, $response); // query category for HIT - $request = $this->prepareRequest($categoryQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $categoryQuery]); + $this->assertCacheHITWithTagsForCategory($categoryId, $response); // query product for HIT - $request = $this->prepareRequest($productQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $productQuery]); + $this->assertCacheHITWithTagsForProduct((string) $product->getId(), $response); - // query product for HIT - $request = $this->prepareRequest($pageQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_p','cms_p_' . $cmsPageId,'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + // query page for HIT + $response = $this->dispatchGraphQlGETRequest(['query' => $pageQuery]); + $this->assertCacheHITWithTagsForCmsPage($cmsPageId, $response); $product->setUrlKey('something-else-that-invalidates-the-cache'); $productRepository->save($product); - $productQuery = $this->getQuery('something-else-that-invalidates-the-cache.html'); + $productQuery = $this->buildQuery('something-else-that-invalidates-the-cache.html'); // query category for MISS - $request = $this->prepareRequest($categoryQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $response = $this->dispatchGraphQlGETRequest(['query' => $categoryQuery]); + $this->assertCacheMISSWithTagsForCategory($categoryId, $response); - // query product for HIT - $request = $this->prepareRequest($productQuery); - $response = $this->graphqlController->dispatch($request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; - $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); - $actualCacheTags = explode(',', $rawActualCacheTags); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } + // query product for MISS + $response = $this->dispatchGraphQlGETRequest(['query' => $productQuery]); + $this->assertCacheMISSWithTagsForProduct((string) $product->getId(), $response); - /** - * Get urlResolver query - * - * @param string $id - * @return string - */ - private function getQuery(string $requestPath) : string - { - $resolverQuery = <<<QUERY -{ - urlResolver(url:"{$requestPath}") - { - id - relative_url - canonical_url - type - } -} -QUERY; - return $resolverQuery; + // query page for HIT + $response = $this->dispatchGraphQlGETRequest(['query' => $pageQuery]); + $this->assertCacheHITWithTagsForCmsPage($cmsPageId, $response); } } diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php index 2a472371fd19f..023421e4cd2bf 100644 --- a/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Fixtures/quote_with_configurable_product.php @@ -118,7 +118,7 @@ $item->setQty(1); $address->setTotalQty(1); $address->addItem($item); - }; + } } $billingAddressData = [ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php index 56dd77d3da17c..790e68ee3a720 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\MysqlMq\Model; /** @@ -23,27 +24,26 @@ class QueueManagementTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->queueManagement = $this->objectManager->create(\Magento\MysqlMq\Model\QueueManagement::class); + $this->queueManagement = $this->objectManager->create(QueueManagement::class); } /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ - public function testAllFlows() + public function testMessageReading() { - $this->queueManagement->addMessageToQueues('topic1', 'messageBody1', ['queue1', 'queue2']); - $this->queueManagement->addMessageToQueues('topic2', 'messageBody2', ['queue2', 'queue3']); - $this->queueManagement->addMessageToQueues('topic3', 'messageBody3', ['queue1', 'queue3']); - $this->queueManagement->addMessageToQueues('topic4', 'messageBody4', ['queue1', 'queue2', 'queue3']); + $this->queueManagement->addMessageToQueues('topic1', 'messageBody1', ['queue1']); + $this->queueManagement->addMessageToQueues('topic2', 'messageBody2', ['queue1']); + $this->queueManagement->addMessageToQueues('topic3', 'messageBody3', ['queue1']); $maxMessagesNumber = 2; - $messages = $this->queueManagement->readMessages('queue3', $maxMessagesNumber); + $messages = $this->queueManagement->readMessages('queue1', $maxMessagesNumber); $this->assertCount($maxMessagesNumber, $messages); $firstMessage = array_shift($messages); - $this->assertEquals('topic2', $firstMessage[QueueManagement::MESSAGE_TOPIC]); - $this->assertEquals('messageBody2', $firstMessage[QueueManagement::MESSAGE_BODY]); - $this->assertEquals('queue3', $firstMessage[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals('topic1', $firstMessage[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody1', $firstMessage[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue1', $firstMessage[QueueManagement::MESSAGE_QUEUE_NAME]); $this->assertEquals( QueueManagement::MESSAGE_STATUS_IN_PROGRESS, $firstMessage[QueueManagement::MESSAGE_STATUS] @@ -55,9 +55,9 @@ public function testAllFlows() $this->assertCount(12, date_parse($firstMessage[QueueManagement::MESSAGE_UPDATED_AT])); $secondMessage = array_shift($messages); - $this->assertEquals('topic3', $secondMessage[QueueManagement::MESSAGE_TOPIC]); - $this->assertEquals('messageBody3', $secondMessage[QueueManagement::MESSAGE_BODY]); - $this->assertEquals('queue3', $secondMessage[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals('topic2', $secondMessage[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody2', $secondMessage[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue1', $secondMessage[QueueManagement::MESSAGE_QUEUE_NAME]); $this->assertEquals( QueueManagement::MESSAGE_STATUS_IN_PROGRESS, $secondMessage[QueueManagement::MESSAGE_STATUS] @@ -67,35 +67,128 @@ public function testAllFlows() $this->assertTrue(is_numeric($secondMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID])); $this->assertEquals(0, $secondMessage[QueueManagement::MESSAGE_NUMBER_OF_TRIALS]); $this->assertCount(12, date_parse($secondMessage[QueueManagement::MESSAGE_UPDATED_AT])); + } + + /** + * @magentoDataFixture Magento/MysqlMq/_files/queues.php + */ + public function testMessageReadingMultipleQueues() + { + $this->queueManagement->addMessageToQueues('topic1', 'messageBody1', ['queue1']); + $this->queueManagement->addMessageToQueues('topic2', 'messageBody2', ['queue1', 'queue2']); + $this->queueManagement->addMessageToQueues('topic3', 'messageBody3', ['queue2']); + + $maxMessagesNumber = 2; + $messages = $this->queueManagement->readMessages('queue1', $maxMessagesNumber); + $this->assertCount($maxMessagesNumber, $messages); + + $message = array_shift($messages); + $this->assertEquals('topic1', $message[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody1', $message[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue1', $message[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals( + QueueManagement::MESSAGE_STATUS_IN_PROGRESS, + $message[QueueManagement::MESSAGE_STATUS] + ); + + $message= array_shift($messages); + $this->assertEquals('topic2', $message[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody2', $message[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue1', $message[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals( + QueueManagement::MESSAGE_STATUS_IN_PROGRESS, + $message[QueueManagement::MESSAGE_STATUS] + ); + + $maxMessagesNumber = 2; + $messages = $this->queueManagement->readMessages('queue2', $maxMessagesNumber); + $this->assertCount($maxMessagesNumber, $messages); + + $message= array_shift($messages); + $this->assertEquals('topic2', $message[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody2', $message[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue2', $message[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals( + QueueManagement::MESSAGE_STATUS_IN_PROGRESS, + $message[QueueManagement::MESSAGE_STATUS] + ); + + $message = array_shift($messages); + $this->assertEquals('topic3', $message[QueueManagement::MESSAGE_TOPIC]); + $this->assertEquals('messageBody3', $message[QueueManagement::MESSAGE_BODY]); + $this->assertEquals('queue2', $message[QueueManagement::MESSAGE_QUEUE_NAME]); + $this->assertEquals( + QueueManagement::MESSAGE_STATUS_IN_PROGRESS, + $message[QueueManagement::MESSAGE_STATUS] + ); + } + + /** + * @magentoDataFixture Magento/MysqlMq/_files/queues.php + */ + public function testChangingMessageStatus() + { + $this->queueManagement->addMessageToQueues('topic1', 'messageBody1', ['queue1']); + $this->queueManagement->addMessageToQueues('topic2', 'messageBody2', ['queue1']); + $this->queueManagement->addMessageToQueues('topic3', 'messageBody3', ['queue1']); + $this->queueManagement->addMessageToQueues('topic4', 'messageBody4', ['queue1']); + + $maxMessagesNumber = 4; + $messages = $this->queueManagement->readMessages('queue1', $maxMessagesNumber); + $this->assertCount($maxMessagesNumber, $messages); + + $firstMessage = array_shift($messages); + $secondMessage = array_shift($messages); + $thirdMessage = array_shift($messages); + $fourthMessage = array_shift($messages); + + $this->queueManagement->changeStatus( + [ + $firstMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID] + ], + QueueManagement::MESSAGE_STATUS_ERROR + ); - /** Mark one message as complete or failed and make sure it is not displayed in the list of read messages */ $this->queueManagement->changeStatus( [ $secondMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID] ], QueueManagement::MESSAGE_STATUS_COMPLETE ); - $messages = $this->queueManagement->readMessages('queue3', $maxMessagesNumber); - $this->assertCount(1, $messages); $this->queueManagement->changeStatus( [ - $firstMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID] + $thirdMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID] ], - QueueManagement::MESSAGE_STATUS_ERROR + QueueManagement::MESSAGE_STATUS_NEW + ); + + $this->queueManagement->changeStatus( + [ + $fourthMessage[QueueManagement::MESSAGE_QUEUE_RELATION_ID] + ], + QueueManagement::MESSAGE_STATUS_RETRY_REQUIRED ); - $messages = $this->queueManagement->readMessages('queue3', $maxMessagesNumber); - $this->assertCount(0, $messages); - /** Ensure that message for retry is still accessible when reading messages from the queue */ - $messages = $this->queueManagement->readMessages('queue2', 1); + $messages = $this->queueManagement->readMessages('queue1'); + $this->assertCount(2, $messages); + } + + /** + * @magentoDataFixture Magento/MysqlMq/_files/queues.php + */ + public function testMessageRetry() + { + $this->queueManagement->addMessageToQueues('topic1', 'messageBody1', ['queue1']); + + $messages = $this->queueManagement->readMessages('queue1', 1); $message = array_shift($messages); $messageRelationId = $message[QueueManagement::MESSAGE_QUEUE_RELATION_ID]; for ($i = 0; $i < 2; $i++) { $this->assertEquals($i, $message[QueueManagement::MESSAGE_NUMBER_OF_TRIALS]); $this->queueManagement->pushToQueueForRetry($message[QueueManagement::MESSAGE_QUEUE_RELATION_ID]); - $messages = $this->queueManagement->readMessages('queue2', 1); + $messages = $this->queueManagement->readMessages('queue1', 1); $message = array_shift($messages); $this->assertEquals($messageRelationId, $message[QueueManagement::MESSAGE_QUEUE_RELATION_ID]); } diff --git a/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php b/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php index 3bd966018b945..ff4f3f8a58bcf 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php +++ b/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php @@ -5,9 +5,24 @@ */ namespace Magento\Payment\Block; +use Magento\Framework\View\Element\Text; +use Magento\Framework\View\LayoutInterface; +use Magento\OfflinePayments\Model\Banktransfer; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\Payment\Block\Info as BlockInfo; +use Magento\Payment\Block\Info\Instructions; +use Magento\Payment\Model\Info; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class InfoTest + */ class InfoTest extends \PHPUnit\Framework\TestCase { /** + * Tests payment info block. + * * @magentoConfigFixture current_store payment/banktransfer/title Bank Method Title * @magentoConfigFixture current_store payment/checkmo/title Checkmo Title Of The Method * @magentoAppArea adminhtml @@ -15,37 +30,32 @@ class InfoTest extends \PHPUnit\Framework\TestCase public function testGetChildPdfAsArray() { /** @var $layout \Magento\Framework\View\Layout */ - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); - $block = $layout->createBlock(\Magento\Payment\Block\Info::class, 'block'); + $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $block = $layout->createBlock(BlockInfo::class, 'block'); - /** @var $paymentInfoBank \Magento\Payment\Model\Info */ - $paymentInfoBank = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Payment\Model\Info::class - ); - $paymentInfoBank->setMethodInstance( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\OfflinePayments\Model\Banktransfer::class - ) + /** @var $paymentInfoBank Info */ + $paymentInfoBank = Bootstrap::getObjectManager()->create( + Info::class ); - /** @var $childBank \Magento\Payment\Block\Info\Instructions */ - $childBank = $layout->addBlock(\Magento\Payment\Block\Info\Instructions::class, 'child.one', 'block'); + $order = Bootstrap::getObjectManager()->create(Order::class); + $banktransferPayment = Bootstrap::getObjectManager()->create(Banktransfer::class); + $paymentInfoBank->setMethodInstance($banktransferPayment); + $paymentInfoBank->setOrder($order); + /** @var $childBank Instructions */ + $childBank = $layout->addBlock(Instructions::class, 'child.one', 'block'); $childBank->setInfo($paymentInfoBank); $nonExpectedHtml = 'non-expected html'; - $childHtml = $layout->addBlock(\Magento\Framework\View\Element\Text::class, 'child.html', 'block'); + $childHtml = $layout->addBlock(Text::class, 'child.html', 'block'); $childHtml->setText($nonExpectedHtml); - /** @var $paymentInfoCheckmo \Magento\Payment\Model\Info */ - $paymentInfoCheckmo = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Payment\Model\Info::class - ); - $paymentInfoCheckmo->setMethodInstance( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\OfflinePayments\Model\Checkmo::class - ) + /** @var $paymentInfoCheckmo Info */ + $paymentInfoCheckmo = Bootstrap::getObjectManager()->create( + Info::class ); + $checkmoPayment = Bootstrap::getObjectManager()->create(Checkmo::class); + $paymentInfoCheckmo->setMethodInstance($checkmoPayment); + $paymentInfoCheckmo->setOrder($order); /** @var $childCheckmo \Magento\OfflinePayments\Block\Info\Checkmo */ $childCheckmo = $layout->addBlock( \Magento\OfflinePayments\Block\Info\Checkmo::class, diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php index cfefd7d3e6d61..3528fd744fe03 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PaypalExpressSetPaymentMethodTest.php @@ -113,7 +113,7 @@ public function testResolve(string $paymentMethod): void } placeOrder(input: {cart_id: "{$maskedCartId}"}) { order { - order_id + order_number } } } @@ -205,11 +205,11 @@ public function testResolve(string $paymentMethod): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php index d55820ccffc17..51745fb6aaf9a 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php @@ -134,7 +134,7 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -190,11 +190,11 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php index 899af918b04bc..024340dc91c26 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProTest.php @@ -122,7 +122,7 @@ public function testResolveCustomer(): void } placeOrder(input: {cart_id: "{$cartId}"}) { order { - order_id + order_number } } } @@ -207,11 +207,11 @@ public function testResolveCustomer(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php index 1b5f14c7df63a..6744b92092818 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalExpressSetPaymentMethodTest.php @@ -112,7 +112,7 @@ public function testResolveGuest(string $paymentMethod): void } placeOrder(input: {cart_id: "{$cartId}"}) { order { - order_id + order_number } } } @@ -191,11 +191,11 @@ public function testResolveGuest(string $paymentMethod): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php index 0e1a74fa817d7..69fe913a91616 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PaypalPayflowProSetPaymentMethodTest.php @@ -122,7 +122,7 @@ public function testResolveGuest(): void } placeOrder(input: {cart_id: "{$cartId}"}) { order { - order_id + order_number } } } @@ -198,11 +198,11 @@ public function testResolveGuest(): void ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php index a8136fda73c09..bf716fe19d17a 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithHostedProTest.php @@ -115,7 +115,7 @@ public function testPlaceOrderWithHostedPro(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -140,11 +140,11 @@ public function testPlaceOrderWithHostedPro(): void $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } @@ -189,7 +189,7 @@ public function testOrderWithHostedProDeclined(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php index f4fe3e7e60fd8..7f13d11ce98cd 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php @@ -133,7 +133,7 @@ public function testResolvePlaceOrderWithPayflowLink(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } @@ -183,11 +183,11 @@ public function testResolvePlaceOrderWithPayflowLink(): void $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] ); $this->assertTrue( - isset($responseData['data']['placeOrder']['order']['order_id']) + isset($responseData['data']['placeOrder']['order']['order_number']) ); $this->assertEquals( 'test_quote', - $responseData['data']['placeOrder']['order']['order_id'] + $responseData['data']['placeOrder']['order']['order_number'] ); } @@ -235,7 +235,7 @@ public function testResolveWithPayflowLinkDeclined(): void } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php index a40a56be5faee..6a757cbb102e4 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php @@ -149,8 +149,8 @@ public function testResolvePlaceOrderWithPaymentsAdvanced(): void $paymentMethod, $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] ); - $this->assertNotEmpty(isset($responseData['data']['placeOrder']['order']['order_id'])); - $this->assertEquals('test_quote', $responseData['data']['placeOrder']['order']['order_id']); + $this->assertNotEmpty(isset($responseData['data']['placeOrder']['order']['order_number'])); + $this->assertEquals('test_quote', $responseData['data']['placeOrder']['order']['order_number']); } /** @@ -265,7 +265,7 @@ private function setPaymentMethodAndPlaceOrder(string $cartId, string $paymentMe } placeOrder(input: {cart_id: "$cartId"}) { order { - order_id + order_number } } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Add/StockTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Add/StockTest.php new file mode 100644 index 0000000000000..2e4da9992642f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Add/StockTest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Controller\Add; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Url\Helper\Data; +use Magento\Customer\Model\Session; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for Magento\ProductAlert\Controller\Add\Stock class. + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class StockTest extends AbstractController +{ + /** + * @var Session + */ + private $customerSession; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Data + */ + private $dataUrlHelper; + + /** + * @var ResourceConnection + */ + protected $resource; + + /** + * Connection adapter + * + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + protected $connectionMock; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + + $this->customerSession = $this->objectManager->get(Session::class); + $this->dataUrlHelper = $this->objectManager->get(Data::class); + + $this->resource = $this->objectManager->get(ResourceConnection::class); + $this->connectionMock = $this->resource->getConnection(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testSubscribeStockNotification() + { + $productId = $this->productRepository->get('simple-out-of-stock')->getId(); + $customerId = 1; + + $this->customerSession->setCustomerId($customerId); + + $encodedParameterValue = $this->getUrlEncodedParameter($productId); + $this->getRequest()->setMethod('GET'); + $this->getRequest()->setQueryValue('product_id', $productId); + $this->getRequest()->setQueryValue(Action::PARAM_NAME_URL_ENCODED, $encodedParameterValue); + $this->dispatch('productalert/add/stock'); + + $select = $this->connectionMock->select()->from($this->resource->getTableName('product_alert_stock')) + ->where('`product_id` LIKE ?', $productId); + $result = $this->connectionMock->fetchAll($select); + $this->assertCount(1, $result); + } + + /** + * @param $productId + * + * @return string + */ + private function getUrlEncodedParameter($productId):string + { + $baseUrl = $this->objectManager->get(StoreManagerInterface::class)->getStore()->getBaseUrl(); + $encodedParameterValue = urlencode( + $this->dataUrlHelper->getEncodedUrl($baseUrl . 'productalert/add/stock/product_id/' . $productId) + ); + + return $encodedParameterValue; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Unsubscribe/StockTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Unsubscribe/StockTest.php new file mode 100644 index 0000000000000..1d56edab9a8a5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Controller/Unsubscribe/StockTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Controller\Unsubscribe; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Customer\Model\Session; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for Magento\ProductAlert\Controller\Unsubscribe\Stock class. + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class StockTest extends AbstractController +{ + /** + * @var Session + */ + private $customerSession; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ResourceConnection + */ + protected $resource; + + /** + * Connection adapter + * + * @var AdapterInterface + */ + protected $connectionMock; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->resource = $this->objectManager->get(ResourceConnection::class); + $this->connectionMock = $this->resource->getConnection(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/ProductAlert/_files/customer_unsubscribe_stock.php + */ + public function testUnsubscribeStockNotification() + { + $customerId = 1; + $productId = $this->productRepository->get('simple-out-of-stock')->getId(); + + $this->customerSession->setCustomerId($customerId); + + $this->getRequest()->setPostValue('product', $productId)->setMethod('POST'); + $this->dispatch('productalert/unsubscribe/stock'); + + $select = $this->connectionMock->select()->from($this->resource->getTableName('product_alert_stock')) + ->where('`product_id` LIKE ?', $productId); + $result = $this->connectionMock->fetchAll($select); + $this->assertCount(0, $result); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php index 0fc98d8d8380b..44f37b34660b6 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php @@ -3,60 +3,123 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ProductAlert\Model; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Area; +use Magento\Framework\Locale\Resolver; +use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\Renderer\Translate as PhraseRendererTranslate; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\Translate; +use Magento\Store\Model\StoreRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CacheCleaner; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\ObjectManager; + /** + * Test for Magento\ProductAlert\Model\Observer + * * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ObserverTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManager */ protected $_objectManager; /** - * @var \Magento\Customer\Model\Session + * @var Observer */ - protected $_customerSession; + private $observer; /** - * @var \Magento\Customer\Helper\View + * @var TransportBuilderMock */ - protected $_customerViewHelper; + private $transportBuilder; + /** + * @inheritDoc + */ public function setUp() { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession = $this->_objectManager->get( - \Magento\Customer\Model\Session::class - ); - $service = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Api\AccountManagementInterface::class - ); + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $this->_objectManager = Bootstrap::getObjectManager(); + $this->observer = $this->_objectManager->get(Observer::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $service = $this->_objectManager->create(AccountManagementInterface::class); $customer = $service->authenticate('customer@example.com', 'password'); - $this->_customerSession->setCustomerDataAsLoggedIn($customer); - $this->_customerViewHelper = $this->_objectManager->create(\Magento\Customer\Helper\View::class); + $customerSession = $this->_objectManager->get(Session::class); + $customerSession->setCustomerDataAsLoggedIn($customer); } /** - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 + * Test process() method * + * @magentoConfigFixture current_store catalog/productalert/allow_price 1 * @magentoDataFixture Magento/ProductAlert/_files/product_alert.php */ public function testProcess() { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - $observer = $this->_objectManager->get(\Magento\ProductAlert\Model\Observer::class); - $observer->process(); - - /** @var \Magento\TestFramework\Mail\Template\TransportBuilderMock $transportBuilder */ - $transportBuilder = $this->_objectManager->get( - \Magento\TestFramework\Mail\Template\TransportBuilderMock::class - ); + $this->observer->process(); $this->assertContains( 'John Smith,', - $transportBuilder->getSentMessage()->getRawMessage() + $this->transportBuilder->getSentMessage()->getRawMessage() ); } + + /** + * Check translations for product alerts + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoConfigFixture current_store catalog/productalert/allow_price 1 + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture fixture_second_store_store general/locale/code pt_BR + * @magentoDataFixture Magento/ProductAlert/_files/product_alert_with_store.php + */ + public function testProcessPortuguese() + { + // get second store + $storeRepository = $this->_objectManager->create(StoreRepository::class); + $secondStore = $storeRepository->get('fixture_second_store'); + + // check if Portuguese language is specified for the second store + CacheCleaner::cleanAll(); + $storeResolver = $this->_objectManager->get(Resolver::class); + $storeResolver->emulate($secondStore->getId()); + $this->assertEquals('pt_BR', $storeResolver->getLocale()); + + // set translation data and check it + $modulesReader = $this->createPartialMock(Reader::class, ['getModuleDir']); + $modulesReader->expects($this->any()) + ->method('getModuleDir') + ->willReturn(dirname(__DIR__) . '/_files/i18n'); + /** @var Translate $translator */ + $translator = $this->_objectManager->create(Translate::class, ['modulesReader' => $modulesReader]); + $translation = [ + 'Price change alert! We wanted you to know that prices have changed for these products:' => + 'Alerta de mudanca de preco! Queriamos que voce soubesse que os precos mudaram para esses produtos:' + ]; + $translator->loadData(); + $this->assertEquals($translation, $translator->getData()); + $this->_objectManager->addSharedInstance($translator, Translate::class); + $this->_objectManager->removeSharedInstance(PhraseRendererTranslate::class); + Phrase::setRenderer($this->_objectManager->create(RendererInterface::class)); + + // dispatch process() method and check sent message + $this->observer->process(); + $message = $this->transportBuilder->getSentMessage()->getRawMessage(); + $expectedText = array_shift($translation); + $this->assertContains('/frontend/Magento/luma/pt_BR/', $message); + $this->assertContains(substr($expectedText, 0, 50), $message); + } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock.php b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock.php new file mode 100644 index 0000000000000..3308b2b1829db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\ProductAlert\Model\ResourceModel\Stock; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$resource = $objectManager->get(Stock::class); + +/** @var \Magento\Framework\Stdlib\DateTime\DateTime $dateTime */ +$dateTime = $objectManager->get(DateTimeFactory::class)->create(); +$date = $dateTime->gmtDate(null, ($dateTime->gmtTimestamp() - 3600)); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productId = $productRepository->get('simple-out-of-stock')->getId(); + +$resource->getConnection()->insert( + $resource->getMainTable(), + [ + 'customer_id' => 1, + 'product_id' => $productId, + 'website_id' => 1, + 'store_id' => 1, + 'add_date' => $date, + 'send_date' => null, + 'send_count' => 0, + 'status' => 0 + ] +); diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock_rollback.php b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock_rollback.php new file mode 100644 index 0000000000000..c01c09df26d10 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/customer_unsubscribe_stock_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ProductAlert\Model\ResourceModel\Stock; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$resource = $objectManager->get(Stock::class); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productId = $productRepository->get('simple-out-of-stock')->getId(); + +$resource->getConnection()->delete( + $resource->getMainTable(), + ['product_id = ?' => $productId] +); diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/_files/i18n/pt_BR.csv b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/i18n/pt_BR.csv new file mode 100644 index 0000000000000..0c8218a78923a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/i18n/pt_BR.csv @@ -0,0 +1 @@ +"Price change alert! We wanted you to know that prices have changed for these products:","Alerta de mudanca de preco! Queriamos que voce soubesse que os precos mudaram para esses produtos:",Magento_ProductAlert diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store.php b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store.php new file mode 100644 index 0000000000000..38f00d49f1d47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\ProductAlert\Model\Price; +use Magento\ProductAlert\Model\Stock; + +require __DIR__ . '/../../../Magento/Customer/_files/customer_for_second_store.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_out_of_stock_without_categories.php'; + +$objectManager = Bootstrap::getObjectManager(); +$price = $objectManager->create(Price::class); +$price->setCustomerId($customer->getId()) + ->setProductId($product->getId()) + ->setPrice($product->getPrice()+1) + ->setWebsiteId(1) + ->setStoreId(2); +$price->save(); + +$stock = $objectManager->create(Stock::class); +$stock->setCustomerId($customer->getId()) + ->setProductId($product->getId()) + ->setWebsiteId(1) + ->setStoreId(2); +$stock->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php b/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php new file mode 100644 index 0000000000000..1b68bc0520ce5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Cron; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for Magento\Sales\Cron\CleanExpiredQuotes class. + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class CleanExpiredQuotesTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CleanExpiredQuotes + */ + private $cleanExpiredQuotes; + + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->cleanExpiredQuotes = $objectManager->get(CleanExpiredQuotes::class); + $this->quoteRepository = $objectManager->get(QuoteRepository::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + } + + /** + * Check if outdated quotes are deleted. + * + * @magentoConfigFixture default_store checkout/cart/delete_quote_after -365 + * @magentoDataFixture Magento/Sales/_files/quotes.php + */ + public function testExecute() + { + $this->cleanExpiredQuotes->execute(); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $totalCount = $this->quoteRepository->getList($searchCriteria)->getTotalCount(); + + $this->assertEquals( + 1, + $totalCount + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php index 1d04a79ae3f84..11499a024b44d 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php @@ -117,9 +117,12 @@ public function testAddComment() $saved = $this->shipmentRepository->save($shipment); $comments = $saved->getComments(); - $actual = array_map(function (CommentInterface $comment) { - return $comment->getComment(); - }, $comments); + $actual = array_map( + function (CommentInterface $comment) { + return $comment->getComment(); + }, + $comments + ); self::assertEquals(2, count($actual)); self::assertEquals([$message1, $message2], $actual); } @@ -144,4 +147,59 @@ private function getOrder(string $incrementId): OrderInterface return array_pop($items); } + + /** + * Check that getTracksCollection() returns only order related tracks. + * + * @magentoDataFixture Magento/Sales/_files/two_orders_with_order_items.php + */ + public function testGetTracksCollection() + { + $order = $this->getOrder('100000001'); + $items = []; + foreach ($order->getItems() as $item) { + $items[$item->getId()] = $item->getQtyOrdered(); + } + /** @var \Magento\Sales\Model\Order\Shipment $shipment */ + $shipment = $this->objectManager->get(ShipmentFactory::class) + ->create($order, $items); + + $tracks = $shipment->getTracksCollection(); + self::assertTrue(empty($tracks->getItems())); + + /** @var ShipmentTrackInterface $track */ + $track = $this->objectManager->create(ShipmentTrackInterface::class); + $track->setNumber('Test Number') + ->setTitle('Test Title') + ->setCarrierCode('Test CODE'); + + $shipment->addTrack($track); + $this->shipmentRepository->save($shipment); + $shipmentTracksCollection = $shipment->getTracksCollection(); + + $secondOrder = $this->getOrder('100000002'); + $secondOrderItems = []; + foreach ($secondOrder->getItems() as $item) { + $secondOrderItems[$item->getId()] = $item->getQtyOrdered(); + } + /** @var \Magento\Sales\Model\Order\Shipment $secondOrderShipment */ + $secondOrderShipment = $this->objectManager->get(ShipmentFactory::class) + ->create($secondOrder, $secondOrderItems); + + /** @var ShipmentTrackInterface $secondShipmentTrack */ + $secondShipmentTrack = $this->objectManager->create(ShipmentTrackInterface::class); + $secondShipmentTrack->setNumber('Test Number2') + ->setTitle('Test Title2') + ->setCarrierCode('Test CODE2'); + + $secondOrderShipment->addTrack($secondShipmentTrack); + $this->shipmentRepository->save($secondOrderShipment); + $secondShipmentTrackCollection = $secondOrderShipment->getTracksCollection(); + + self::assertEquals($shipmentTracksCollection->getColumnValues('id'), [$track->getEntityId()]); + self::assertEquals( + $secondShipmentTrackCollection->getColumnValues('id'), + [$secondShipmentTrack->getEntityId()] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php index d6fe86b6232d3..25f759e7b1b97 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php @@ -3,8 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\ResourceModel; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class OrderTest extends \PHPUnit\Framework\TestCase { /** @@ -22,27 +32,48 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->resourceModel = $this->objectManager->create(\Magento\Sales\Model\ResourceModel\Order::class); $this->orderIncrementId = '100000001'; + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); } + /** + * @inheritdoc + */ protected function tearDown() { $registry = $this->objectManager->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId($this->orderIncrementId); - $order->delete(); + $orderCollection = $this->objectManager->create(OrderCollectionFactory::class)->create(); + foreach ($orderCollection as $order) { + $order->delete(); + } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); + $defaultStore = $this->storeRepository->get('default'); + $this->storeManager->setCurrentStore($defaultStore->getId()); + parent::tearDown(); } @@ -108,4 +139,29 @@ public function testSaveOrder() $this->assertNotNull($order->getCreatedAt()); $this->assertNotNull($order->getUpdatedAt()); } + + /** + * Check that store name with length within 255 chars can be saved in table sales_order + * + * @magentoDataFixture Magento/Store/_files/store_with_long_name.php + * @magentoDbIsolation disabled + * @return void + */ + public function testSaveStoreName() + { + $store = $this->storeRepository->get('test_2'); + $this->storeManager->setCurrentStore($store->getId()); + $eventManager = $this->objectManager->get(ManagerInterface::class); + $eventManager->dispatch('store_add', ['store' => $store]); + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $payment = $this->objectManager->create(\Magento\Sales\Model\Order\Payment::class); + $payment->setMethod('checkmo'); + $order->setStoreId($store->getId())->setPayment($payment); + $this->resourceModel->save($order); + $orderRepository = $this->objectManager->create(\Magento\Sales\Api\OrderRepositoryInterface::class); + $order = $orderRepository->get($order->getId()); + $this->assertEquals(255, strlen($order->getStoreName())); + $this->assertContains($store->getWebsite()->getName(), $order->getStoreName()); + $this->assertContains($store->getGroup()->getName(), $order->getStoreName()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quotes.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes.php new file mode 100644 index 0000000000000..3c4c164f0a01f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Store\Model\StoreRepository; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +require dirname(dirname(__DIR__)) . '/Store/_files/second_store.php'; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = $objectManager->get(QuoteFactory::class); +/** @var QuoteRepository $quoteRepository */ +$quoteRepository = $objectManager->get(QuoteRepository::class); +/** @var StoreRepository $storeRepository */ +$storeRepository = $objectManager->get(StoreRepository::class); + +$defaultStore = $storeRepository->getActiveStoreByCode('default'); +$secondStore = $storeRepository->getActiveStoreByCode('fixture_second_store'); + +$quotes = [ + 'quote for first store' => [ + 'store' => $defaultStore->getId(), + ], + 'quote for second store' => [ + 'store' => $secondStore->getId(), + ], +]; + +foreach ($quotes as $quoteData) { + $quote = $quoteFactory->create(); + $quote->setStoreId($quoteData['store']); + $quoteRepository->save($quote); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_rollback.php new file mode 100644 index 0000000000000..7b7fd615e5340 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quotes_rollback.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var QuoteRepository $quoteRepository */ +$quoteRepository = $objectManager->get(QuoteRepository::class); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->create(); +$items = $quoteRepository->getList($searchCriteria) + ->getItems(); +foreach ($items as $item) { + $quoteRepository->delete($item); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_with_order_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_with_order_items.php new file mode 100644 index 0000000000000..ade37aed49d59 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_with_order_items.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; + +require 'default_rollback.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; +/** @var \Magento\Catalog\Model\Product $product */ + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var Payment $payment */ +$secondPayment = $objectManager->create(Payment::class); +$secondPayment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$secondOrderItem = $objectManager->create(OrderItem::class); +$secondOrderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$secondOrder = $objectManager->create(Order::class); +$secondOrder->setIncrementId('100000002') + ->setState(Order::STATE_PROCESSING) + ->setStatus($secondOrder->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($secondOrderItem) + ->setPayment($secondPayment); +$orderRepository->save($secondOrder); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free.php new file mode 100644 index 0000000000000..2d5035523e161 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\GroupManagement; +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Rule $salesRule */ +$salesRule = $objectManager->create(Rule::class); +$salesRule->setData( + [ + 'name' => 'Buy 3 And Get 1 Free', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [], + 'simple_action' => Rule::BUY_X_GET_Y_ACTION, + 'discount_amount' => 1, + 'discount_step' => 3, + 'stop_rules_processing' => 0, + 'store_labels' => [0 => ' Get 1 item free for every 3 you buy'], + 'website_ids' => [ + $objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + ] +); +$objectManager->get(\Magento\SalesRule\Model\ResourceModel\Rule::class)->save($salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free_rollback.php new file mode 100644 index 0000000000000..f6866a8066ee3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/buy_3_get_1_free_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// phpcs:ignore Magento2.Security.IncludeFile +require __DIR__ . '/rules_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items.php new file mode 100644 index 0000000000000..f8a0d65b00a10 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\GroupManagement; +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$websiteId = Bootstrap::getObjectManager()->get(StoreManagerInterface::class) + ->getWebsite() + ->getId(); + +/** @var Rule $salesRule */ +$salesRule = $objectManager->create(Rule::class); +$salesRule->setData( + [ + 'name' => '10% Off on orders with two items', + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_NO_COUPON, + 'simple_action' => 'by_percent', + 'discount_amount' => 10, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'is_advanced' => 1, + 'website_ids' => [$websiteId], + 'store_labels' => [ + + 'store_id' => 0, + 'store_label' => '10% off with two items_Label', + + ] + ] +); + +$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' => 'quote_item_qty', + 'operator' => '>=', + 'value' => '2', + 'is_value_processed' => false, + ], + ], + ], + ], + ] +); + +$salesRule->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items_rollback.php new file mode 100644 index 0000000000000..f6866a8066ee3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_qty_more_than_2_items_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +// phpcs:ignore Magento2.Security.IncludeFile +require __DIR__ . '/rules_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_categories_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_categories_rollback.php index 968193b26fe17..0f7de0efe41f6 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_categories_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_categories_rollback.php @@ -3,11 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; + +$objectManager = Bootstrap::getObjectManager(); /** @var Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry = $objectManager->get(\Magento\Framework\Registry::class); /** @var Magento\SalesRule\Model\Rule $rule */ $rule = $registry->registry('_fixture/Magento_SalesRule_Multiple_Categories'); $rule->delete(); + +// logic to delete the category that was created as part of the rules_category fixture +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', 'Category 1') + ->create(); + +/** @var CategoryListInterface $categoryList */ +$categoryList = $objectManager->get(CategoryListInterface::class); +$categories = $categoryList->getList($searchCriteria) + ->getItems(); + +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + +foreach ($categories as $category) { + $categoryRepository->delete($category); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category.php index 939d6d9e28200..0a83a1f65d875 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category.php @@ -15,16 +15,23 @@ 'simple_action' => 'by_percent', 'discount_amount' => 50, 'discount_step' => 0, - 'stop_rules_processing' => 1, + 'stop_rules_processing' => 0, 'website_ids' => [ \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Store\Model\StoreManagerInterface::class )->getWebsite()->getId() + ], + 'store_labels' => [ + + 'store_id' => 0, + 'store_label' => 'TestRule_Label', + ] ] ); -$salesRule->getConditions()->loadArray([ +$salesRule->getConditions()->loadArray( + [ 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, 'attribute' => null, 'operator' => null, @@ -52,7 +59,8 @@ ], ], ], -]); + ] +); $salesRule->save(); @@ -67,7 +75,7 @@ )->setParentId( 2 )->setPath( - '1/2/333' + '1/2/66' )->setLevel( 2 )->setAvailableSortBy( diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category_rollback.php index 7ad18fc73c687..1ecd20a1f518f 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rules_category_rollback.php @@ -11,3 +11,16 @@ $rule = $registry->registry('_fixture/Magento_SalesRule_Category'); $rule->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(66); +if ($category->getId()) { + $category->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php index 0e158821f1802..a1a99ecd32b89 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php @@ -14,7 +14,7 @@ use Zend\Stdlib\Parameters; /** - * Class RequestPreprocessorTest @covers \Magento\Store\App\FrontController\Plugin\RequestPreprocessor. + * Tests \Magento\Store\App\FrontController\Plugin\RequestPreprocessor. */ class RequestPreprocessorTest extends \Magento\TestFramework\TestCase\AbstractController { @@ -24,6 +24,28 @@ class RequestPreprocessorTest extends \Magento\TestFramework\TestCase\AbstractCo * @var string */ private $baseUrl; + /** + * @var array; + */ + private $config; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + $this->config = []; + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + $this->setConfig($this->config); + parent::tearDown(); + } /** * Test non-secure POST request is redirected right away on completely secure frontend. @@ -62,6 +84,115 @@ public function testHttpsPassSecureLoginPost() $this->setFrontendCompletelySecureRollback(); } + /** + * Test auto redirect to base URL + * + * @param array $config + * @param string $requestUrl + * @param string $redirectUrl + * @dataProvider autoRedirectToBaseURLDataProvider + */ + public function testAutoRedirectToBaseURL(array $config, string $requestUrl, string $redirectUrl) + { + $request = [ + 'REQUEST_SCHEME' => parse_url($requestUrl, PHP_URL_SCHEME), + 'SERVER_NAME' => parse_url($requestUrl, PHP_URL_HOST), + 'SCRIPT_NAME' => '/index.php', + 'SCRIPT_FILENAME' => 'index.php', + 'REQUEST_URI' => parse_url($requestUrl, PHP_URL_PATH), + ]; + $this->setConfig($config); + $this->setServer($request); + $app = $this->_objectManager->create(\Magento\Framework\App\Http::class, ['_request' => $this->getRequest()]); + $this->_response = $app->launch(); + $this->assertRedirect($this->equalTo($redirectUrl)); + } + + /** + * @return array + */ + public function autoRedirectToBaseURLDataProvider(): array + { + $baseConfig = [ + 'web/unsecure/base_url' => 'http://magento.com/us/', + 'web/session/use_frontend_sid' => 0, + 'web/seo/use_rewrites' => 1, + ]; + + return [ + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c/d/e.html', + 'redirectUrl' => 'http://magento.com/us/a/b/c/d/e.html' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c/d.html', + 'redirectUrl' => 'http://magento.com/us/a/b/c/d.html' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c.html', + 'redirectUrl' => 'http://magento.com/us/a/b/c.html' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b.html', + 'redirectUrl' => 'http://magento.com/us/a/b.html' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a.html', + 'redirectUrl' => 'http://magento.com/us/a.html' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c/d/e', + 'redirectUrl' => 'http://magento.com/us/a/b/c/d/e' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c/d', + 'redirectUrl' => 'http://magento.com/us/a/b/c/d' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b/c', + 'redirectUrl' => 'http://magento.com/us/a/b/c' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a/b', + 'redirectUrl' => 'http://magento.com/us/a/b' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/a', + 'redirectUrl' => 'http://magento.com/us/a' + ], + [ + 'config' => $baseConfig, + 'request' => 'http://magento.com/', + 'redirectUrl' => 'http://magento.com/us/' + ], + [ + 'config' => array_merge($baseConfig, ['web/seo/use_rewrites' => 0]), + 'request' => 'http://magento.com/', + 'redirectUrl' => 'http://magento.com/us/index.php/' + ], + [ + 'config' => array_merge($baseConfig, ['web/seo/use_rewrites' => 0]), + 'request' => 'http://magento.com/a/b/c/d.html', + 'redirectUrl' => 'http://magento.com/us/index.php/a/b/c/d.html' + ], + [ + 'config' => array_merge($baseConfig, ['web/seo/use_rewrites' => 0]), + 'request' => 'http://magento.com/a/b/c/d', + 'redirectUrl' => 'http://magento.com/us/index.php/a/b/c/d' + ], + ]; + } + /** * Assert response is redirect with https protocol. * @@ -83,22 +214,26 @@ private function assertResponseRedirect(Response $response, string $redirectUrl) */ private function prepareRequest(bool $isSecure = false) { - $post = new Parameters([ - 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), - 'login' => [ - 'username' => 'customer@example.com', - 'password' => 'password' + $post = new Parameters( + [ + 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), + 'login' => [ + 'username' => 'customer@example.com', + 'password' => 'password' + ] ] - ]); + ); $request = $this->getRequest(); $request->setMethod(\Magento\TestFramework\Request::METHOD_POST); $request->setRequestUri('customer/account/loginPost/'); $request->setPost($post); if ($isSecure) { - $server = new Parameters([ - 'HTTPS' => 'on', - 'SERVER_PORT' => 443 - ]); + $server = new Parameters( + [ + 'HTTPS' => 'on', + 'SERVER_PORT' => 443 + ] + ); $request->setServer($server); } @@ -151,4 +286,45 @@ private function setFrontendCompletelySecureRollback() $reinitibleConfig = $this->_objectManager->create(ReinitableConfigInterface::class); $reinitibleConfig->reinit(); } + + private function setConfig(array $config): void + { + foreach ($config as $path => $value) { + $model = $this->_objectManager->create(Value::class); + $model->load($path, 'path'); + if (!isset($this->config[$path])) { + $this->config[$path] = $model->getValue(); + } + if (!$model->getPath()) { + $model->setPath($path); + } + if ($value !== null) { + $model->setValue($value); + $model->save(); + } elseif ($model->getId()) { + $model->delete(); + } + } + $this->_objectManager->create(ReinitableConfigInterface::class)->reinit(); + } + + private function setServer(array $server) + { + $request = $this->getRequest(); + $properties = [ + 'baseUrl', + 'basePath', + 'requestUri', + 'originalPathInfo', + 'pathInfo', + ]; + $reflection = new \ReflectionClass($request); + + foreach ($properties as $name) { + $property = $reflection->getProperty($name); + $property->setAccessible(true); + $property->setValue($request, null); + } + $request->setServer(new Parameters($server)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name.php b/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name.php new file mode 100644 index 0000000000000..f1beaee683b82 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var $store \Magento\Store\Model\Store */ +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +$storeName = str_repeat('a', 255); +if (!$store->load('test', 'code')->getId()) { + $store->setData( + [ + 'code' => 'test_2', + 'website_id' => '1', + 'group_id' => '1', + 'name' => $storeName, + 'sort_order' => '10', + 'is_active' => '1', + ] + ); + $store->save(); +} else { + if ($store->getId()) { + /** @var \Magento\TestFramework\Helper\Bootstrap $registry */ + $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Registry::class + ); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $store->delete(); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store->setData( + [ + 'code' => 'test_2', + 'website_id' => '1', + 'group_id' => '1', + 'name' => $storeName, + 'sort_order' => '10', + 'is_active' => '1', + ] + ); + $store->save(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name_rollback.php new file mode 100644 index 0000000000000..5fe19e1e97df1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/store_with_long_name_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Store $store */ +$store = $objectManager->get(Store::class); +$store->load('test_2', 'code'); +if ($store->getId()) { + $store->delete(); +} + +/** @var Store $store */ +$store = $objectManager->get(Store::class); +$store->load('test_2', 'code'); +if ($store->getId()) { + $store->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Controller/UrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Controller/UrlRewriteTest.php index 74d6edaef847b..795f29332876a 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Controller/UrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Controller/UrlRewriteTest.php @@ -15,6 +15,7 @@ class UrlRewriteTest extends AbstractController { /** * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php + * @magentoDbIsolation disabled * * @covers \Magento\UrlRewrite\Controller\Router::match * @covers \Magento\UrlRewrite\Model\Storage\DbStorage::doFindOneByData diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php index 8ea9fdcd744f1..d7da1389ac847 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -62,6 +62,8 @@ protected function setUp() * * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled * @return void * @throws StoreSwitcher\CannotSwitchStoreException * @throws \Magento\Framework\Exception\NoSuchEntityException @@ -71,7 +73,7 @@ public function testSwitchToNonExistingPage(): void $fromStore = $this->getStoreByCode('default'); $toStore = $this->getStoreByCode('fixture_second_store'); - $this->setBaseUrl($toStore); + $this->setBaseUrl($toStore, 'http://domain.com/'); $product = $this->productRepository->get('simple333'); @@ -79,12 +81,14 @@ public function testSwitchToNonExistingPage(): void $expectedUrl = $toStore->getBaseUrl(); $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); + $this->setBaseUrl($toStore, 'http://localhost/'); } /** * Testing store switching with existing cms pages * * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php + * @magentoDbIsolation disabled * @return void * @throws StoreSwitcher\CannotSwitchStoreException * @throws \Magento\Framework\Exception\NoSuchEntityException @@ -120,13 +124,13 @@ public function testSwitchCmsPageToAnotherStore(): void * Set base url to store. * * @param StoreInterface $targetStore + * @param string $baseUrl * @return void */ - private function setBaseUrl(StoreInterface $targetStore): void + private function setBaseUrl(StoreInterface $targetStore, string $baseUrl): void { $configValue = $this->objectManager->create(Value::class); $configValue->load('web/unsecure/base_url', 'path'); - $baseUrl = 'http://domain.com/'; if (!$configValue->getPath()) { $configValue->setPath('web/unsecure/base_url'); } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php index 8fec06284a78c..22d95751fbf26 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php @@ -11,6 +11,15 @@ $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); +/** @var Magento\Cms\Api\PageRepositoryInterface $pageRepository */ +$pageRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Magento\Cms\Api\PageRepositoryInterface::class +); + +$pageRepository->deleteById('page-a'); +$pageRepository->deleteById('page-b'); +$pageRepository->deleteById('page-c'); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); @@ -20,7 +29,7 @@ ->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); $collection = $urlRewriteCollection ->addFieldToFilter('entity_type', 'custom') - ->addFieldToFilter('request_path', ['page-a', 'page-b', 'page-c']) + ->addFieldToFilter('target_path', ['page-a/', 'page-a', 'page-b', 'page-c']) ->load() ->walk('delete'); diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index a6fc9999ad267..ba273f3d1b738 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -6,7 +6,6 @@ namespace Magento\User\Model; -use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Encryption\Encryptor; /** @@ -29,11 +28,6 @@ class UserTest extends \PHPUnit\Framework\TestCase */ protected static $_newRole; - /** - * @var Json - */ - private $serializer; - /** * @var Encryptor */ @@ -47,9 +41,6 @@ protected function setUp() $this->_dateTime = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\Stdlib\DateTime::class ); - $this->serializer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - Json::class - ); $this->encryptor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( Encryptor::class ); @@ -133,7 +124,7 @@ public function testSaveExtra() $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); $this->_model->saveExtra(['test' => 'val']); $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); - $extra = $this->serializer->unserialize($this->_model->getExtra()); + $extra = $this->_model->getExtra(); $this->assertEquals($extra['test'], 'val'); } diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute_rollback.php index 1d6e15b2e9a97..bced91f4a07b0 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/fixed_product_attribute_rollback.php @@ -8,7 +8,7 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/* @var EavAttribute $attribute */ +/* @var \Magento\Eav\Model\Entity\Attribute $attribute */ $attribute = $objectManager->get(\Magento\Eav\Model\Entity\Attribute::class); $attribute->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'fixed_product_attribute'); $attribute->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt.php b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt.php index 0e67a8947a79d..59a5516bd67d3 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt.php +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt.php @@ -16,6 +16,7 @@ $entityTypeId = $entityModel->setType(Product::ENTITY)->getTypeId(); $groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ $attribute = Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class ); @@ -27,12 +28,15 @@ $groupId )->setAttributeSetId( $attributeSetId +)->setFrontendLabel( + 'fpt_for_all_front_label' )->setFrontendInput( 'weee' )->setIsUserDefined( 1 )->save(); +/** @var Product $product */ $product = Bootstrap::getObjectManager()->create(Product::class); $product->setTypeId( 'simple' diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt_rollback.php b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt_rollback.php index d0305f461eb03..a32c7c03a9f04 100644 --- a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_fpt_rollback.php @@ -13,12 +13,13 @@ /** @var $product \Magento\Catalog\Model\Product */ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->load(101); -if ($product->getId()) { + +$product = $product->loadByAttribute('sku', 'simple-with-ftp'); +if ($product && $product->getId()) { $product->delete(); } -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->load('fpt_for_all', 'attribute_code'); diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt.php b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt.php new file mode 100644 index 0000000000000..92137a59b1dbd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/product_with_fpt.php'; + +/** @var \Magento\Catalog\Setup\CategorySetup $installer */ +$installer = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$entityModel = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Entity::class); +$entityTypeId = $entityModel->setType(Product::ENTITY)->getTypeId(); +$groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); + +/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ +$attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); +$attribute->setAttributeCode( + 'fixed_product_attribute' +)->setEntityTypeId( + $entityTypeId +)->setAttributeGroupId( + $groupId +)->setAttributeSetId( + $attributeSetId +)->setFrontendLabel( + 'fixed_product_attribute_front_label' +)->setFrontendInput( + 'weee' +)->setIsUserDefined( + 1 +)->save(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + +$product = $product->loadByAttribute('sku', 'simple-with-ftp'); +if ($product && $product->getId()) { + $product->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10.00, 'delete' => '']] + )->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt_rollback.php b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt_rollback.php new file mode 100644 index 0000000000000..5e87438492dce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Weee/_files/product_with_two_fpt_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +$attribute->load('fixed_product_attribute', 'attribute_code'); +if ($attribute->getId()) { + $attribute->delete(); +} + +require __DIR__ . '/product_with_fpt_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/Collection/GridTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/Collection/GridTest.php new file mode 100644 index 0000000000000..f7d7199134013 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/Collection/GridTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\ResourceModel\Item\Collection; + +use Magento\Customer\Controller\RegistryConstants; +use Magento\Customer\Model\Customer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Registry; +use Magento\Store\Model\Website; +use PHPUnit\Framework\TestCase; + +/** + * Class to test wishlist collection by customer functionality + * + * @magentoAppArea adminhtml + */ +class GridTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Registry + */ + private $registryManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = ObjectManager::getInstance(); + $this->registryManager = $this->objectManager->get(Registry::class); + } + + /** + * Test to load wishlist collection by customer on second website + * + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Wishlist/_files/wishlist_on_second_website.php + */ + public function testLoadOnSecondWebsite() + { + $customer = $this->loadCustomer(); + $this->registryManager->register(RegistryConstants::CURRENT_CUSTOMER_ID, $customer->getId()); + + $gridCollection = $this->objectManager->get(Grid::class); + $this->assertNotEmpty($gridCollection->getItems()); + } + + /** + * Load customer in second website + * + * @return Customer + */ + private function loadCustomer(): Customer + { + /** @var $website Website */ + $website = $this->objectManager->get(Website::class); + $website->load('newwebsite', 'code'); + + /** @var Customer $customer */ + $customer = $this->objectManager->get(Customer::class); + $customer->setWebsiteId($website->getId()); + $customer->loadByEmail('customer2@example.com'); + + return $customer; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website.php new file mode 100644 index 0000000000000..6d8051cf060f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/products_with_websites_and_stores.php'; +require __DIR__ . '/../../../Magento/Customer/_files/customer_non_default_website_id.php'; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\Wishlist; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$simpleProduct = $productRepository->get('simple-2'); + +/* @var $wishlist Wishlist */ +$wishlist = Bootstrap::getObjectManager()->create(Wishlist::class); +$wishlist->loadByCustomerId($customer->getId(), true); +$wishlist->addNewItem($simpleProduct); +$wishlist->setSharingCode('fixture_unique_code') + ->setShared(1) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website_rollback.php new file mode 100644 index 0000000000000..49b3e120f7354 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_on_second_website_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/products_with_websites_and_stores_rollback.php'; +require __DIR__ . '/../../../Magento/Customer/_files/customer_non_default_website_id_rollback.php'; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js index c2a20c8339c7c..8ea992ce30e4f 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js @@ -35,7 +35,8 @@ define([ beforeAll(function (done) { window.checkoutConfig = { quoteData: { - entityId: 1 + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + entity_Id: 1 }, formKey: 'formKey' }; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.test.js new file mode 100644 index 0000000000000..bf0ff3466c529 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.test.js @@ -0,0 +1,80 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Swatches/js/swatch-renderer' +], function ($, SwatchRenderer) { + 'use strict'; + + describe('Testing "_RenderSwatchOptions" method of SwatchRenderer Widget', function () { + var widget, + html, + optionConfig, + attribute, + optionId = 2, + swathImageHeight = '60', + swathImageWidth = '70', + swathThumbImageHeight = '40', + swathThumbImageWidth = '50'; + + beforeEach(function () { + widget = new SwatchRenderer(); + attribute = { + id: 1, + options: [{ + id: optionId + }] + }; + widget.options = { + classes: { + optionClass: 'swatch-option' + }, + jsonSwatchConfig: { + 1: { + 2: { + type: 2 + } + } + }, + jsonSwatchImageSizeConfig: { + swatchImage: { + width: swathImageWidth, + height: swathImageHeight + }, + swatchThumb: { + width: swathThumbImageWidth, + height: swathThumbImageHeight + } + } + }; + optionConfig = widget.options.jsonSwatchConfig[attribute.id]; + html = $(widget._RenderSwatchOptions(attribute, 'option-label-control-id-1'))[0]; + }); + + it('check if swatch config has attribute id', function () { + expect(widget.options.jsonSwatchConfig.hasOwnProperty(attribute.id)).toEqual(true); + }); + + it('check if option config has option id', function () { + expect(optionConfig.hasOwnProperty(optionId)).toEqual(true); + }); + + it('check swatch thumbnail image height attribute', function () { + expect(html.hasAttribute('thumb-height')).toBe(true); + expect(html.getAttribute('thumb-height')).toEqual(swathThumbImageHeight); + }); + + it('check swatch thumbnail image width attribute', function () { + expect(html.hasAttribute('thumb-width')).toBe(true); + expect(html.getAttribute('thumb-width')).toEqual(swathThumbImageWidth); + }); + + it('check swatch image styles', function () { + expect(html.style.height).toEqual(swathImageHeight + 'px'); + expect(html.style.width).toEqual(swathImageWidth + 'px'); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/lib/mage/translate.test.js b/dev/tests/js/jasmine/tests/lib/mage/translate.test.js index c87cfa227c1aa..dc6c6ce7eb966 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/translate.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/translate.test.js @@ -9,11 +9,16 @@ define([ ], function ($) { 'use strict'; + // be careful with test variation order as one variation can affect another one describe('Test for mage/translate jQuery plugin', function () { it('works with one string as parameter', function () { $.mage.translate.add('Hello World!'); expect('Hello World!').toEqual($.mage.translate.translate('Hello World!')); }); + it('works with translation alias __', function () { + $.mage.translate.add('Hello World!'); + expect('Hello World!').toEqual($.mage.__('Hello World!')); + }); it('works with one array as parameter', function () { $.mage.translate.add(['Hello World!', 'Bonjour tout le monde!']); expect('Hello World!').toEqual($.mage.translate.translate('Hello World!')); @@ -40,10 +45,6 @@ define([ $.mage.translate.add('Hello World!', 'Bonjour tout le monde!'); expect('Bonjour tout le monde!').toEqual($.mage.translate.translate('Hello World!')); }); - it('works with translation alias __', function () { - $.mage.translate.add('Hello World!'); - expect('Hello World!').toEqual($.mage.__('Hello World!')); - }); }); }); diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php new file mode 100644 index 0000000000000..e38fba8558bad --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CodeMessDetector\Rule\Design; + +use PHPMD\AbstractNode; +use PHPMD\AbstractRule; +use PHPMD\Node\ClassNode; +use PHPMD\Node\MethodNode; +use PDepend\Source\AST\ASTMethod; +use PHPMD\Rule\MethodAware; + +/** + * Detect PHP serialization aware methods. + */ +class SerializationAware extends AbstractRule implements MethodAware +{ + /** + * @inheritDoc + * + * @param ASTMethod|MethodNode $method + */ + public function apply(AbstractNode $method) + { + if ($method->getName() === '__wakeup' || $method->getName() === '__sleep') { + $this->addViolation($method, [$method->getName(), $method->getParent()->getFullQualifiedName()]); + } + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml index 53f2fe4a0084e..5f2461812bab7 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml +++ b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml @@ -60,6 +60,31 @@ class OrderProcessor $currentOrder = $this->session->get('current_order'); ... } +} + ]]> + </example> + </rule> + <rule name="SerializationAware" + class="Magento\CodeMessDetector\Rule\Design\SerializationAware" + message="{1} has {0} method and is PHP serialization aware - PHP serialization must be avoided."> + <description> + <![CDATA[ +Using PHP serialization must be avoided in Magento for security reasons and for prevention of unexpected behaviour. + ]]> + </description> + <priority>2</priority> + <properties /> + <example> + <![CDATA[ +class MyModel extends AbstractModel +{ + + ....... + + public function __sleep() + { + ..... + } } ]]> </example> diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php index 5c342614f94f0..f9630fd8cc05e 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/PhtmlTemplateTest.php @@ -7,6 +7,8 @@ */ namespace Magento\Test\Legacy; +use Magento\Framework\Component\ComponentRegistrar; + /** * Static test for phtml template files. */ @@ -105,4 +107,84 @@ function ($file) { \Magento\Framework\App\Utility\Files::init()->getPhtmlFiles() ); } + + public function testJsComponentsAreProperlyInitializedInDataMageInitAttribute() + { + $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker( + /** + * JS components in data-mage-init attributes should be initialized not in php. + * JS components should be initialized in templates for them to be properly statically analyzed for bundling. + * + * @param string $file + */ + function ($file) { + $whiteList = $this->getWhiteList(); + if (!in_array($file, $whiteList, true) + && (strpos($file, '/view/frontend/templates/') !== false + || strpos($file, '/view/base/templates/') !== false) + ) { + self::assertNotRegExp( + '/data-mage-init=(?:\'|")(?!\s*{\s*"[^"]+")/', + file_get_contents($file), + 'Please do not initialize JS component in php. Do it in template.' + ); + } + }, + \Magento\Framework\App\Utility\Files::init()->getPhtmlFiles() + ); + } + + /** + * @return array + */ + private function getWhiteList() + { + $whiteListFiles = []; + $componentRegistrar = new ComponentRegistrar(); + foreach ($this->getFilesData('data_mage_init/whitelist.php') as $fileInfo) { + $whiteListFiles[] = $componentRegistrar->getPath(ComponentRegistrar::MODULE, $fileInfo[0]) + . DIRECTORY_SEPARATOR . $fileInfo[1]; + } + return $whiteListFiles; + } + + /** + * @param string $filePattern + * @return array + */ + private function getFilesData($filePattern) + { + $result = []; + foreach (glob(__DIR__ . '/_files/initialize_javascript/' . $filePattern) as $file) { + $fileData = include $file; + $result = array_merge($result, $fileData); + } + return $result; + } + + public function testJsComponentsAreProperlyInitializedInXMagentoInitAttribute() + { + $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker( + /** + * JS components in x-magento-init attributes should be initialized not in php. + * JS components should be initialized in templates for them to be properly statically analyzed for bundling. + * + * @param string $file + */ + function ($file) { + if (strpos($file, '/view/frontend/templates/') !== false + || strpos($file, '/view/base/templates/') !== false + ) { + self::assertNotRegExp( + '@x-magento-init.>(?!\s*+{\s*"[^"]+"\s*:\s*{\s*"[\w/-]+")@i', + file_get_contents($file), + 'Please do not initialize JS component in php. Do it in template.' + ); + } + }, + \Magento\Framework\App\Utility\Files::init()->getPhtmlFiles() + ); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/initialize_javascript/data_mage_init/whitelist.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/initialize_javascript/data_mage_init/whitelist.php new file mode 100644 index 0000000000000..a77b7b28864ec --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/initialize_javascript/data_mage_init/whitelist.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** + * List of templates with data-mage-init attribute where JS component is not correctly called. + * + * JS component is initialized in php here. These templates cannot be refactored easily. This list consists of + * module name and template path within module. + */ +return [ + ['Magento_Braintree', 'view/frontend/templates/paypal/button_shopping_cart.phtml'] +]; diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index 0e3b5fa3d341c..e65a9a089da9e 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -45,5 +45,6 @@ <!-- Magento Specific Rules --> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/AllPurposeAction" /> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/CookieAndSessionMisuse" /> + <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/SerializationAware" /> </ruleset> diff --git a/lib/internal/Magento/Framework/Acl/AclResource/Config/Converter/Dom.php b/lib/internal/Magento/Framework/Acl/AclResource/Config/Converter/Dom.php index 68762a8a6c046..7f7a4761b17a2 100644 --- a/lib/internal/Magento/Framework/Acl/AclResource/Config/Converter/Dom.php +++ b/lib/internal/Magento/Framework/Acl/AclResource/Config/Converter/Dom.php @@ -5,10 +5,13 @@ */ namespace Magento\Framework\Acl\AclResource\Config\Converter; +/** + * @inheritDoc + */ class Dom implements \Magento\Framework\Config\ConverterInterface { /** - * {@inheritdoc} + * @inheritdoc * * @param \DOMDocument $source * @return array @@ -39,6 +42,7 @@ protected function _convertResourceNode(\DOMNode $resourceNode) $resourceAttributes = $resourceNode->attributes; $idNode = $resourceAttributes->getNamedItem('id'); if ($idNode === null) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Attribute "id" is required for ACL resource.'); } $resourceData['id'] = $idNode->nodeValue; @@ -53,7 +57,7 @@ protected function _convertResourceNode(\DOMNode $resourceNode) $sortOrderNode = $resourceAttributes->getNamedItem('sortOrder'); $resourceData['sortOrder'] = $sortOrderNode !== null ? (int)$sortOrderNode->nodeValue : 0; $disabledNode = $resourceAttributes->getNamedItem('disabled'); - $resourceData['disabled'] = $disabledNode !== null && $disabledNode->nodeValue == 'true' ? true : false; + $resourceData['disabled'] = $disabledNode !== null && $disabledNode->nodeValue == 'true'; // convert child resource nodes if needed $resourceData['children'] = []; /** @var $childNode \DOMNode */ diff --git a/lib/internal/Magento/Framework/Amqp/composer.json b/lib/internal/Magento/Framework/Amqp/composer.json index 7963561420586..8c0a3e3070aaf 100644 --- a/lib/internal/Magento/Framework/Amqp/composer.json +++ b/lib/internal/Magento/Framework/Amqp/composer.json @@ -12,7 +12,7 @@ "require": { "magento/framework": "*", "php": "~7.1.3||~7.2.0||~7.3.0", - "php-amqplib/php-amqplib": "~2.7.0" + "php-amqplib/php-amqplib": "~2.7.0|~2.10.0" }, "autoload": { "psr-4": { diff --git a/lib/internal/Magento/Framework/Api/SearchResults.php b/lib/internal/Magento/Framework/Api/SearchResults.php index ad58d0a752653..cf1f11463f6ad 100644 --- a/lib/internal/Magento/Framework/Api/SearchResults.php +++ b/lib/internal/Magento/Framework/Api/SearchResults.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Api; /** * SearchResults Service Data Object used for the search service requests + * + * @SuppressWarnings(PHPMD.NumberOfChildren) */ class SearchResults extends AbstractSimpleObject implements SearchResultsInterface { diff --git a/lib/internal/Magento/Framework/Api/SearchResultsInterface.php b/lib/internal/Magento/Framework/Api/SearchResultsInterface.php index d2bc3053b8d6e..ba72685a80f49 100644 --- a/lib/internal/Magento/Framework/Api/SearchResultsInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchResultsInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Api; diff --git a/lib/internal/Magento/Framework/App/AreaList/Proxy.php b/lib/internal/Magento/Framework/App/AreaList/Proxy.php index d080e4cabbd87..105ddd3727906 100644 --- a/lib/internal/Magento/Framework/App/AreaList/Proxy.php +++ b/lib/internal/Magento/Framework/App/AreaList/Proxy.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\App\AreaList; /** - * Application area list + * Proxy for area list. */ class Proxy extends \Magento\Framework\App\AreaList implements \Magento\Framework\ObjectManager\NoninterceptableInterface @@ -57,9 +58,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to other objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -70,6 +74,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/App/Cache/Frontend/Pool.php b/lib/internal/Magento/Framework/App/Cache/Frontend/Pool.php index 30cb4a67b9edd..a4c9fb4380651 100644 --- a/lib/internal/Magento/Framework/App/Cache/Frontend/Pool.php +++ b/lib/internal/Magento/Framework/App/Cache/Frontend/Pool.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\App\Cache\Frontend; use Magento\Framework\App\Cache\Type\FrontendPool; @@ -55,6 +56,7 @@ public function __construct( /** * Create instances of every cache frontend known to the system. + * * Method is to be used for delayed initialization of the iterator. * * @return void @@ -77,18 +79,21 @@ protected function _initialize() protected function _getCacheSettings() { /* - * Merging is intentionally implemented through array_merge() instead of array_replace_recursive() - * to avoid "inheritance" of the default settings that become irrelevant as soon as cache storage type changes + * Merging is intentionally implemented through array_replace_recursive() instead of array_merge(), because even + * though some settings may become irrelevant when the cache storage type is changed, they don't do any harm + * and can be overwritten when needed. + * Also array_merge leads to unexpected behavior, for for example by dropping the + * default cache_dir setting from di.xml when a cache id_prefix is configured in app/etc/env.php. */ $cacheInfo = $this->deploymentConfig->getConfigData(FrontendPool::KEY_CACHE); if (null !== $cacheInfo) { - return array_merge($this->_frontendSettings, $cacheInfo[FrontendPool::KEY_FRONTEND_CACHE]); + return array_replace_recursive($this->_frontendSettings, $cacheInfo[FrontendPool::KEY_FRONTEND_CACHE]); } return $this->_frontendSettings; } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Framework\Cache\FrontendInterface */ @@ -99,7 +104,7 @@ public function current() } /** - * {@inheritdoc} + * @inheritdoc */ public function key() { @@ -108,7 +113,7 @@ public function key() } /** - * {@inheritdoc} + * @inheritdoc */ public function next() { @@ -117,7 +122,7 @@ public function next() } /** - * {@inheritdoc} + * @inheritdoc */ public function rewind() { @@ -126,7 +131,7 @@ public function rewind() } /** - * {@inheritdoc} + * @inheritdoc */ public function valid() { diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig/Reader.php b/lib/internal/Magento/Framework/App/DeploymentConfig/Reader.php index ff7077213c5c3..a53ea9423d449 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig/Reader.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig/Reader.php @@ -16,7 +16,6 @@ /** * Deployment configuration reader. * Loads the merged configuration from config files. - * * @see FileReader The reader for specific configuration file */ class Reader @@ -107,11 +106,9 @@ public function load($fileKey = null) } } } else { - $configFiles = $this->configFilePool->getPaths(); - $allFilesData = []; - $result = []; - foreach (array_keys($configFiles) as $fileKey) { - $configFile = $path . '/' . $this->configFilePool->getPath($fileKey); + $configFiles = $this->getFiles(); + foreach ($configFiles as $file) { + $configFile = $path . '/' . $file; if ($fileDriver->isExists($configFile)) { $fileData = include $configFile; if (!is_array($fileData)) { @@ -120,7 +117,6 @@ public function load($fileKey = null) } else { continue; } - $allFilesData[$configFile] = $fileData; if ($fileData) { $result = array_replace_recursive($result, $fileData); } @@ -136,6 +132,8 @@ public function load($fileKey = null) * @param string $pathConfig The path config * @param bool $ignoreInitialConfigFiles Whether ignore custom pools * @return array + * @throws FileSystemException + * @throws RuntimeException * @deprecated 100.2.0 Magento does not support custom config file pools since 2.2.0 version * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/lib/internal/Magento/Framework/App/MaintenanceMode.php b/lib/internal/Magento/Framework/App/MaintenanceMode.php index e813522a01513..11347e4220c26 100644 --- a/lib/internal/Magento/Framework/App/MaintenanceMode.php +++ b/lib/internal/Magento/Framework/App/MaintenanceMode.php @@ -110,7 +110,7 @@ public function setAddresses($addresses) throw new \InvalidArgumentException("One or more IP-addresses is expected (comma-separated)\n"); } $result = $this->flagDir->writeFile(self::IP_FILENAME, $addresses); - return false !== $result ? true : false; + return false !== $result; } /** diff --git a/lib/internal/Magento/Framework/App/ProductMetadata.php b/lib/internal/Magento/Framework/App/ProductMetadata.php index c9fde94352a71..631dba8273bcd 100644 --- a/lib/internal/Magento/Framework/App/ProductMetadata.php +++ b/lib/internal/Magento/Framework/App/ProductMetadata.php @@ -8,12 +8,13 @@ namespace Magento\Framework\App; use Magento\Framework\Composer\ComposerFactory; -use \Magento\Framework\Composer\ComposerJsonFinder; -use \Magento\Framework\App\Filesystem\DirectoryList; -use \Magento\Framework\Composer\ComposerInformation; +use Magento\Framework\Composer\ComposerJsonFinder; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Composer\ComposerInformation; /** * Class ProductMetadata + * * @package Magento\Framework\App */ class ProductMetadata implements ProductMetadataInterface @@ -28,6 +29,11 @@ class ProductMetadata implements ProductMetadataInterface */ const PRODUCT_NAME = 'Magento'; + /** + * Cache key for Magento product version + */ + private const MAGENTO_PRODUCT_VERSION_CACHE_KEY = 'magento-product-version'; + /** * Product version * @@ -46,12 +52,19 @@ class ProductMetadata implements ProductMetadataInterface */ private $composerInformation; + /** + * @var CacheInterface + */ + private $cache; + /** * @param ComposerJsonFinder $composerJsonFinder + * @param CacheInterface|null $cache */ - public function __construct(ComposerJsonFinder $composerJsonFinder) + public function __construct(ComposerJsonFinder $composerJsonFinder, CacheInterface $cache = null) { $this->composerJsonFinder = $composerJsonFinder; + $this->cache = $cache ?: ObjectManager::getInstance()->get(CacheInterface::class); } /** @@ -61,6 +74,9 @@ public function __construct(ComposerJsonFinder $composerJsonFinder) */ public function getVersion() { + if ($cachedVersion = $this->cache->load(self::MAGENTO_PRODUCT_VERSION_CACHE_KEY)) { + $this->version = $cachedVersion; + } if (!$this->version) { if (!($this->version = $this->getSystemPackageVersion())) { if ($this->getComposerInformation()->isMagentoRoot()) { @@ -69,6 +85,7 @@ public function getVersion() $this->version = 'UNKNOWN'; } } + $this->cache->save($this->version, self::MAGENTO_PRODUCT_VERSION_CACHE_KEY); } return $this->version; } diff --git a/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php b/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php index f15ce494e9bb4..e15a5151942f0 100644 --- a/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php +++ b/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php @@ -32,7 +32,7 @@ class InvalidRequestException extends RuntimeException /** * @param ResponseInterface|ResultInterface|NotFoundException $replaceResult * Use this result instead of calling an action instance, - * if NotFoundException is given the the default 404 mechanism will be triggered. + * if NotFoundException is given the default 404 mechanism will be triggered. * @param Phrase[]|null $messages Messages to show to client * as error messages. */ @@ -45,6 +45,8 @@ public function __construct($replaceResult, ?array $messages = null) } /** + * Return replaced result + * * @return ResponseInterface|ResultInterface|NotFoundException */ public function getReplaceResult() @@ -53,6 +55,8 @@ public function getReplaceResult() } /** + * Return messages + * * @return Phrase[]|null */ public function getMessages(): ?array diff --git a/lib/internal/Magento/Framework/App/Response/Http.php b/lib/internal/Magento/Framework/App/Response/Http.php index e6fff90837d9d..279ae9d9649f6 100644 --- a/lib/internal/Magento/Framework/App/Response/Http.php +++ b/lib/internal/Magento/Framework/App/Response/Http.php @@ -1,10 +1,9 @@ <?php /** - * HTTP response - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\App\Response; use Magento\Framework\App\Http\Context; @@ -17,7 +16,7 @@ use Magento\Framework\Session\Config\ConfigInterface; /** - * HTTP response + * HTTP Response. * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -182,10 +181,13 @@ public function representJson($content) } /** - * Sleep magic method. + * Remove links to other objects. * * @return string[] * @codeCoverageIgnore + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -197,6 +199,9 @@ public function __sleep() * * @return void * @codeCoverageIgnore + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php index 09dda9727b937..863a6d7d836d4 100644 --- a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php +++ b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php @@ -60,9 +60,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to other objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -73,6 +76,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/PoolTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/PoolTest.php index bfa37311884ba..5ec3dd658737b 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/PoolTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/PoolTest.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Cache\Frontend\Pool; use Magento\Framework\App\Cache\Type\FrontendPool; +/** + * And another docblock to make the sniff shut up. + */ class PoolTest extends \PHPUnit\Framework\TestCase { /** @@ -111,25 +114,38 @@ public function testInitializationParams( public function initializationParamsDataProvider() { return [ - 'default frontend, default settings' => [ + 'no deployment config, default settings' => [ ['frontend' => []], [Pool::DEFAULT_FRONTEND_ID => ['default_option' => 'default_value']], ['default_option' => 'default_value'], ], - 'default frontend, overridden settings' => [ + 'deployment config, default settings' => [ + ['frontend' => [Pool::DEFAULT_FRONTEND_ID => ['configured_option' => 'configured_value']]], + [Pool::DEFAULT_FRONTEND_ID => ['default_option' => 'default_value']], + ['configured_option' => 'configured_value', 'default_option' => 'default_value'], + ], + 'deployment config, overridden settings' => [ ['frontend' => [Pool::DEFAULT_FRONTEND_ID => ['configured_option' => 'configured_value']]], - [Pool::DEFAULT_FRONTEND_ID => ['ignored_option' => 'ignored_value']], + [Pool::DEFAULT_FRONTEND_ID => ['configured_option' => 'default_value']], ['configured_option' => 'configured_value'], ], - 'custom frontend, default settings' => [ - ['frontend' => []], + 'deployment config, default settings, overridden settings' => [ + ['frontend' => [Pool::DEFAULT_FRONTEND_ID => ['configured_option' => 'configured_value']]], + [Pool::DEFAULT_FRONTEND_ID => [ + 'configured_option' => 'default_value', + 'default_setting' => 'default_value' + ]], + ['configured_option' => 'configured_value', 'default_setting' => 'default_value'], + ], + 'custom deployent config, default settings' => [ + ['frontend' => ['custom' => ['configured_option' => 'configured_value']]], ['custom' => ['default_option' => 'default_value']], - ['default_option' => 'default_value'], + ['configured_option' => 'configured_value', 'default_option' => 'default_value'], ], - 'custom frontend, overridden settings' => [ + 'custom deployent config, default settings, overridden settings' => [ ['frontend' => ['custom' => ['configured_option' => 'configured_value']]], - ['custom' => ['ignored_option' => 'ignored_value']], - ['configured_option' => 'configured_value'], + ['custom' => ['default_option' => 'default_value', 'configured_option' => 'default_value']], + ['configured_option' => 'configured_value', 'default_option' => 'default_value'], ] ]; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php index 8f8399263384c..8a8bebb4d2f81 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/ReaderTest.php @@ -43,17 +43,18 @@ protected function setUp() ->willReturn(__DIR__ . '/_files'); $this->fileDriver = $this->createMock(File::class); $this->fileDriver - ->expects($this->any()) ->method('isExists') - ->will($this->returnValueMap([ - [__DIR__ . '/_files/config.php', true], - [__DIR__ . '/_files/custom.php', true], - [__DIR__ . '/_files/duplicateConfig.php', true], - [__DIR__ . '/_files/env.php', true], - [__DIR__ . '/_files/mergeOne.php', true], - [__DIR__ . '/_files/mergeTwo.php', true], - [__DIR__ . '/_files/nonexistent.php', false] - ])); + ->willReturnMap( + [ + [__DIR__.'/_files/config.php', true], + [__DIR__.'/_files/custom.php', true], + [__DIR__.'/_files/duplicateConfig.php', true], + [__DIR__.'/_files/env.php', true], + [__DIR__.'/_files/mergeOne.php', true], + [__DIR__.'/_files/mergeTwo.php', true], + [__DIR__.'/_files/nonexistent.php', false] + ] + ); $this->driverPool = $this->createMock(DriverPool::class); $this->driverPool ->expects($this->any()) @@ -152,8 +153,9 @@ public function testLoadInvalidConfigurationFileWithFileKey() * @expectedException \Magento\Framework\Exception\RuntimeException * @expectedExceptionMessageRegExp /Invalid configuration file: \'.*\/\_files\/emptyConfig\.php\'/ * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ - public function testLoadInvalidConfigurationFile() + public function testLoadInvalidConfigurationFile(): void { $fileDriver = $this->getMockBuilder(File::class) ->disableOriginalConstructor() @@ -173,7 +175,7 @@ public function testLoadInvalidConfigurationFile() $configFilePool = $this->getMockBuilder(ConfigFilePool::class) ->disableOriginalConstructor() ->getMock(); - $configFilePool->expects($this->exactly(2)) + $configFilePool->expects($this->once()) ->method('getPaths') ->willReturn( [ @@ -181,15 +183,6 @@ public function testLoadInvalidConfigurationFile() 'testConfig' => 'emptyConfig.php' ] ); - $configFilePool->expects($this->exactly(2)) - ->method('getPath') - ->withConsecutive( - [$this->identicalTo('configKeyOne')], - [$this->identicalTo('testConfig')] - )->willReturnOnConsecutiveCalls( - 'config.php', - 'emptyConfig.php' - ); $object = new Reader($this->dirList, $driverPool, $configFilePool); $object->load(); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php index efb35b7321c3b..9be68b379900a 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php @@ -290,45 +290,6 @@ public function testRepresentJson() $this->assertEquals('json_string', $this->model->getBody('default')); } - /** - * - * @expectedException \RuntimeException - * @expectedExceptionMessage ObjectManager isn't initialized - */ - public function testWakeUpWithException() - { - /* ensure that the test preconditions are met */ - $objectManagerClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); - $instanceProperty = $objectManagerClass->getProperty('_instance'); - $instanceProperty->setAccessible(true); - $instanceProperty->setValue(null); - - $this->model->__wakeup(); - $this->assertNull($this->cookieMetadataFactoryMock); - $this->assertNull($this->cookieManagerMock); - } - - /** - * Test for the magic method __wakeup - * - * @covers \Magento\Framework\App\Response\Http::__wakeup - */ - public function testWakeUpWith() - { - $objectManagerMock = $this->createMock(\Magento\Framework\App\ObjectManager::class); - $objectManagerMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Stdlib\CookieManagerInterface::class) - ->will($this->returnValue($this->cookieManagerMock)); - $objectManagerMock->expects($this->at(1)) - ->method('get') - ->with(\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class) - ->will($this->returnValue($this->cookieMetadataFactoryMock)); - - \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); - $this->model->__wakeup(); - } - public function testSetXFrameOptions() { $value = 'DENY'; diff --git a/lib/internal/Magento/Framework/Backup/Archive/Tar.php b/lib/internal/Magento/Framework/Backup/Archive/Tar.php index ca8e7caf9884d..4ac40c584ee20 100644 --- a/lib/internal/Magento/Framework/Backup/Archive/Tar.php +++ b/lib/internal/Magento/Framework/Backup/Archive/Tar.php @@ -15,6 +15,9 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +/** + * Class to work with tar archives + */ class Tar extends \Magento\Framework\Archive\Tar { /** @@ -25,8 +28,7 @@ class Tar extends \Magento\Framework\Archive\Tar protected $_skipFiles = []; /** - * Overridden \Magento\Framework\Archive\Tar::_createTar method that does the same actions as it's parent but - * filters files using \Magento\Framework\Backup\Filesystem\Iterator\Filter + * Method same as it's parent but filters files using \Magento\Framework\Backup\Filesystem\Iterator\Filter * * @param bool $skipRoot * @param bool $finalize @@ -38,9 +40,8 @@ class Tar extends \Magento\Framework\Archive\Tar protected function _createTar($skipRoot = false, $finalize = false) { $path = $this->_getCurrentFile(); - $filesystemIterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path), + new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS), RecursiveIteratorIterator::SELF_FIRST ); @@ -50,6 +51,10 @@ protected function _createTar($skipRoot = false, $finalize = false) ); foreach ($iterator as $item) { + // exclude symlinks to do not get duplicates after follow symlinks in RecursiveDirectoryIterator + if ($item->isLink()) { + continue; + } $this->_setCurrentFile($item->getPathname()); $this->_packAndWriteCurrentFile(); } diff --git a/lib/internal/Magento/Framework/Code/Reader/NamespaceResolver.php b/lib/internal/Magento/Framework/Code/Reader/NamespaceResolver.php index 8c22170a126fe..f0ff31964512c 100644 --- a/lib/internal/Magento/Framework/Code/Reader/NamespaceResolver.php +++ b/lib/internal/Magento/Framework/Code/Reader/NamespaceResolver.php @@ -50,7 +50,7 @@ public function resolveNamespace($type, array $availableNamespaces) ) { $name = explode(self::NS_SEPARATOR, $type); $unqualifiedName = $name[0]; - $isQualifiedName = count($name) > 1 ? true : false; + $isQualifiedName = count($name) > 1; if (isset($availableNamespaces[$unqualifiedName])) { $namespace = $availableNamespaces[$unqualifiedName]; if ($isQualifiedName) { @@ -101,16 +101,22 @@ public function getImportedNamespaces(array $fileContent) $imports[$importsCount][] = $item; } foreach ($imports as $import) { - $import = array_filter($import, function ($token) { - $whitelist = [T_NS_SEPARATOR, T_STRING, T_AS]; - if (isset($token[0]) && in_array($token[0], $whitelist)) { - return true; + $import = array_filter( + $import, + function ($token) { + $whitelist = [T_NS_SEPARATOR, T_STRING, T_AS]; + if (isset($token[0]) && in_array($token[0], $whitelist)) { + return true; + } + return false; } - return false; - }); - $import = array_map(function ($element) { - return $element[1]; - }, $import); + ); + $import = array_map( + function ($element) { + return $element[1]; + }, + $import + ); $import = array_values($import); if ($import[0] === self::NS_SEPARATOR) { array_shift($import); diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index 34fd6316ce454..6aab9c03ff7b2 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -19,6 +19,7 @@ use Magento\Setup\Application; use Magento\Setup\Console\CompilerPreparation; use Magento\Setup\Model\ObjectManagerProvider; +use Psr\Log\LoggerInterface; use Symfony\Component\Console; use Magento\Framework\Config\ConfigOptionsListConstants; @@ -61,6 +62,11 @@ class Cli extends Console\Application */ private $objectManager; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param string $name the application name * @param string $version the application version @@ -94,6 +100,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') parent::__construct($name, $version); $this->serviceManager->setService(\Symfony\Component\Console\Application::class, $this); + $this->logger = $this->objectManager->get(LoggerInterface::class); } /** @@ -107,7 +114,9 @@ public function doRun(Console\Input\InputInterface $input, Console\Output\Output try { $exitCode = parent::doRun($input, $output); } catch (\Exception $e) { - $output->writeln($e->getTraceAsString()); + $errorMessage = $e->getMessage() . PHP_EOL . $e->getTraceAsString(); + $this->logger->error($errorMessage); + $this->initException = $e; } if ($this->initException) { diff --git a/lib/internal/Magento/Framework/Console/Test/Unit/CliTest.php b/lib/internal/Magento/Framework/Console/Test/Unit/CliTest.php new file mode 100644 index 0000000000000..6e7bf049c4301 --- /dev/null +++ b/lib/internal/Magento/Framework/Console/Test/Unit/CliTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Console\Test\Unit; + +use Magento\Framework\Console\Cli; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Test for Magento\Framework\Console\Cli class. + */ +class CliTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Cli + */ + private $cli; + + /** + * @var InputInterface|MockObject + */ + private $inputMock; + + /** + * @var OutputInterface|MockObject + */ + private $outputMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->inputMock = $this->getMockBuilder(InputInterface::class) + ->getMockForAbstractClass(); + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + $this->cli = new Cli(); + } + + /** + * Make sure exception message is displayed and trace is logged. + * + * @expectedException \Exception + * @expectedExceptionMessage Test message + */ + public function testDoRunExceptionLogging() + { + $e = new \Exception('Test message'); + $this->inputMock->expects($this->once())->method('getFirstArgument')->willThrowException($e); + $loggerMock = $this->createMock(LoggerInterface::class); + $loggerMock->expects($this->once()) + ->method('error') + ->with($e->getMessage() . PHP_EOL . $e->getTraceAsString()); + $this->injectMock($loggerMock, 'logger'); + + $this->cli->doRun($this->inputMock, $this->outputMock); + } + + /** + * Inject mock to Cli property. + * + * @param MockObject $mockObject + * @param string $propertyName + * @throws \ReflectionException + */ + private function injectMock(MockObject $mockObject, string $propertyName): void + { + $reflection = new \ReflectionClass(Cli::class); + $reflectionProperty = $reflection->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->cli, $mockObject); + } +} diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 7399845215bb5..c0aa06f2d11da 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -42,7 +42,7 @@ class Select extends \Zend_Db_Select const STRAIGHT_JOIN = 'straightjoin'; /** - * Sql straight join + * Straight join SQL directive. */ const SQL_STRAIGHT_JOIN = 'STRAIGHT_JOIN'; @@ -400,7 +400,7 @@ public function useStraightJoin($flag = true) /** * Render STRAIGHT_JOIN clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderStraightjoin($sql) @@ -452,7 +452,7 @@ public function orderRand($field = null) /** * Render FOR UPDATE clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderForupdate($sql) @@ -509,10 +509,13 @@ public function assemble() } /** - * Sleep magic method. + * Remove links to other objects. * * @return string[] * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -532,6 +535,9 @@ public function __sleep() * * @return void * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php index b6d0803759842..f3029a7ac2bd0 100644 --- a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php +++ b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php @@ -56,9 +56,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to other objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -69,6 +72,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index c44916fb5af6f..cd5e6bd0a2f59 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -889,6 +889,9 @@ public function hasFlag($flag) * * @return string[] * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -907,6 +910,9 @@ public function __sleep() * * @return void * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index 8a22c9a1ce4fc..dc4b71caf5bee 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -896,6 +896,9 @@ private function getMainTableAlias() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -908,6 +911,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Data/Form/AbstractForm.php b/lib/internal/Magento/Framework/Data/Form/AbstractForm.php index f3b26dc7a9bfa..4a082d71ddd4a 100644 --- a/lib/internal/Magento/Framework/Data/Form/AbstractForm.php +++ b/lib/internal/Magento/Framework/Data/Form/AbstractForm.php @@ -67,9 +67,11 @@ public function __construct(Factory $factoryElement, CollectionFactory $factoryC * Please override this one instead of overriding real __construct constructor * * @return void + * @codingStandardsIgnoreStart */ protected function _construct() { + //@codingStandardsIgnoreEnd } /** @@ -137,14 +139,14 @@ public function addElement(AbstractElement $element, $after = null) /** * Add child element * - * if $after parameter is false - then element adds to end of collection - * if $after parameter is null - then element adds to befin of collection - * if $after parameter is string - then element adds after of the element with some id + * If $after parameter is false - then element adds to end of collection + * If $after parameter is null - then element adds to befin of collection + * If $after parameter is string - then element adds after of the element with some id * - * @param string $elementId - * @param string $type - * @param array $config - * @param bool|string|null $after + * @param string $elementId + * @param string $type + * @param array $config + * @param bool|string|null $after * @return AbstractElement */ public function addField($elementId, $type, $config, $after = false) diff --git a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php index d8bb7a06e5b7d..42d58daec2c93 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php @@ -57,9 +57,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to other objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -70,6 +73,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php index 452357003630c..4fc9ec992a3ef 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php @@ -28,6 +28,11 @@ protected function setUp() $this->key = substr(__CLASS__, -32, 32); } + /** + * @param int $length + * + * @return string + */ protected function getRandomString(int $length): string { $result = ''; @@ -48,18 +53,33 @@ private function requireCipherInfo() } } + /** + * @param string $cipherName + * @param string $modeName + * + * @return int + */ private function getKeySize(string $cipherName, string $modeName): int { $this->requireCipherInfo(); return self::$cipherInfo[$cipherName][$modeName]['key_size']; } + /** + * @param string $cipherName + * @param string $modeName + * + * @return int + */ private function getInitVectorSize(string $cipherName, string $modeName): int { $this->requireCipherInfo(); return self::$cipherInfo[$cipherName][$modeName]['iv_size']; } + /** + * @return array + */ public function getCipherModeCombinations(): array { $result = []; @@ -87,6 +107,9 @@ public function testConstructor(string $cipher, string $mode) $this->assertEquals($initVector, $crypt->getInitVector()); } + /** + * @return array + */ public function getConstructorExceptionData(): array { $key = substr(__CLASS__, -32, 32); @@ -130,6 +153,9 @@ public function testConstructorDefaults() $this->assertEquals($cryptExpected->getInitVector(), $cryptActual->getInitVector()); } + /** + * @return array + */ public function getCryptData(): array { $fixturesFilename = __DIR__ . '/../Crypt/_files/_crypt_fixtures.php'; diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php index 85faa0aa4676f..b7a9ae3bc1e38 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php @@ -33,6 +33,9 @@ public function testIsValid($key, $expected = true) $this->assertEquals($expected, $this->keyValidator->isValid($key)); } + /** + * @return array + */ public function isValidDataProvider() : array { return [ diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php index fe0e6b37666b7..74e6cac7d77b3 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php @@ -58,7 +58,7 @@ public function validate( } if (mb_strpos($actualPath, $realDirectoryPath) !== 0 - && $path .DIRECTORY_SEPARATOR !== $realDirectoryPath + && rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR !== $realDirectoryPath ) { throw new ValidatorException( new Phrase( diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 3c6d2b7321b82..f23ed87971a39 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -53,9 +53,7 @@ public function __construct( protected function assertWritable($path) { if ($this->isWritable($path) === false) { - $path = (!$this->driver->isFile($path)) - ? $this->getAbsolutePath($this->path, $path) - : $this->getAbsolutePath($path); + $path = $this->getAbsolutePath($path); throw new FileSystemException(new \Magento\Framework\Phrase('The path "%1" is not writable.', [$path])); } } diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/PathValidatorTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/PathValidatorTest.php new file mode 100644 index 0000000000000..1fe4596759ebd --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/PathValidatorTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Unit Test for \Magento\Framework\Filesystem\Directory\PathValidator + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Filesystem\Test\Unit\Directory; + +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; + +class PathValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * \Magento\Framework\Filesystem\Driver + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $driver; + + /** + * @var \Magento\Framework\Filesystem\Directory\PathValidator + */ + protected $pathValidator; + + /** + * Set up + */ + protected function setUp() + { + $this->driver = $this->createMock(\Magento\Framework\Filesystem\Driver\File::class); + $this->pathValidator = new \Magento\Framework\Filesystem\Directory\PathValidator( + $this->driver + ); + } + + /** + * Tear down + */ + protected function tearDown() + { + $this->pathValidator = null; + } + + /** + * @param string $directoryPath + * @param string $path + * @param string $scheme + * @param bool $absolutePath + * @param string $prefix + * @dataProvider validateDataProvider + */ + public function testValidate($directoryPath, $path, $scheme, $absolutePath, $prefix) + { + $this->driver->expects($this->exactly(2)) + ->method('getRealPathSafety') + ->willReturnMap( + [ + [$directoryPath, $directoryPath], + [null, $prefix . $directoryPath . ltrim($path, '/')], + ] + ); + + $this->assertNull( + $this->pathValidator->validate($directoryPath, $path, $scheme, $absolutePath) + ); + } + + /** + * @return array + */ + public function validateDataProvider() + { + return [ + ['/directory/path/', '/directory/path/', '/', false, '/://'], + ['/directory/path/', '/var/.regenerate', null, false, ''], + ]; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php index 2fdb3df5f6d71..2c498b0b7fd03 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php @@ -31,7 +31,7 @@ public function __construct( } /** - * Check the the environment config to determine if introspection should be disabled. + * Check the environment config to determine if introspection should be disabled. * * @return bool */ diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php index dfb8b748469b8..ebcbbeaa04ca0 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/ScalarTypes.php @@ -21,7 +21,7 @@ class ScalarTypes public function isScalarType(string $typeName) : bool { $standardTypes = \GraphQL\Type\Definition\Type::getStandardTypes(); - return isset($standardTypes[$typeName]) ? true : false; + return isset($standardTypes[$typeName]); } /** diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php index 2cc2da62e71cb..17d7482607622 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php @@ -1,22 +1,24 @@ <?php /** - * Base HTTP response object - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\HTTP\PhpEnvironment; +/** + * Base HTTP response object + */ class Response extends \Zend\Http\PhpEnvironment\Response implements \Magento\Framework\App\Response\HttpInterface { /** * Flag; is this response a redirect? + * * @var boolean */ protected $isRedirect = false; /** - * {@inheritdoc} + * @inheritdoc */ public function getHeader($name) { @@ -29,8 +31,7 @@ public function getHeader($name) } /** - * Send the response, including all headers, rendering exceptions if so - * requested. + * Send the response, including all headers, rendering exceptions if so requested. * * @return void */ @@ -40,7 +41,7 @@ public function sendResponse() } /** - * {@inheritdoc} + * @inheritdoc */ public function appendBody($value) { @@ -50,7 +51,7 @@ public function appendBody($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBody($value) { @@ -60,6 +61,7 @@ public function setBody($value) /** * Clear body + * * @return $this */ public function clearBody() @@ -69,7 +71,7 @@ public function clearBody() } /** - * {@inheritdoc} + * @inheritdoc */ public function setHeader($name, $value, $replace = false) { @@ -84,7 +86,7 @@ public function setHeader($name, $value, $replace = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function clearHeader($name) { @@ -111,7 +113,7 @@ public function clearHeaders() } /** - * {@inheritdoc} + * @inheritdoc */ public function setRedirect($url, $code = 302) { @@ -122,7 +124,7 @@ public function setRedirect($url, $code = 302) } /** - * {@inheritdoc} + * @inheritdoc */ public function setHttpResponseCode($code) { @@ -130,14 +132,14 @@ public function setHttpResponseCode($code) throw new \InvalidArgumentException('Invalid HTTP response code'); } - $this->isRedirect = (300 <= $code && 307 >= $code) ? true : false; + $this->isRedirect = (300 <= $code && 307 >= $code); $this->setStatusCode($code); return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public function setStatusHeader($httpCode, $version = null, $phrase = null) { @@ -152,7 +154,7 @@ public function setStatusHeader($httpCode, $version = null, $phrase = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getHttpResponseCode() { @@ -170,7 +172,10 @@ public function isRedirect() } /** + * @inheritDoc + * * @return string[] + * @SuppressWarnings(PHPMD.SerializationAware) */ public function __sleep() { diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/MultiDimensionProviderTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/MultiDimensionProviderTest.php index b55ace9bdec3d..60bbc092469c6 100644 --- a/lib/internal/Magento/Framework/Indexer/Test/Unit/MultiDimensionProviderTest.php +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/MultiDimensionProviderTest.php @@ -210,6 +210,11 @@ public function testMultiDimensionProviderWithMixedDataProvider() } } + /** + * @param $dimensions + * + * @return \PHPUnit\Framework\MockObject\MockObject + */ private function getDimensionProviderMock($dimensions) { $dimensionProviderMock = $this->getMockBuilder(DimensionProviderInterface::class) @@ -233,6 +238,12 @@ function () use ($dimensions) { return $dimensionProviderMock; } + /** + * @param string $name + * @param string $value + * + * @return \PHPUnit\Framework\MockObject\MockObject + */ private function getDimensionMock(string $name, string $value) { $dimensionMock = $this->getMockBuilder(Dimension::class) diff --git a/lib/internal/Magento/Framework/Interception/Interceptor.php b/lib/internal/Magento/Framework/Interception/Interceptor.php index 07600c5168181..ccc311c5b3426 100644 --- a/lib/internal/Magento/Framework/Interception/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Interceptor.php @@ -62,6 +62,9 @@ public function ___callParent($method, array $arguments) * Calls parent class sleep if defined, otherwise provides own implementation * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -78,6 +81,9 @@ public function __sleep() * Causes Interceptor to be initialized * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Locale/Config.php b/lib/internal/Magento/Framework/Locale/Config.php index 499c3bd26a3ae..f02ba78ccc3e0 100644 --- a/lib/internal/Magento/Framework/Locale/Config.php +++ b/lib/internal/Magento/Framework/Locale/Config.php @@ -91,7 +91,8 @@ class Config implements \Magento\Framework\Locale\ConfigInterface 'sk_SK', /*Slovak (Slovakia)*/ 'sl_SI', /*Slovenian (Slovenia)*/ 'sq_AL', /*Albanian (Albania)*/ - 'sr_Cyrl_RS', /*Serbian (Serbia)*/ + 'sr_Cyrl_RS', /*Serbian (Cyrillic, Serbia)*/ + 'sr_Latn_RS', /*Serbian (Latin, Serbia)*/ 'sv_SE', /*Swedish (Sweden)*/ 'sv_FI', /*Swedish (Finland)*/ 'sw_KE', /*Swahili (Kenya)*/ diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/ConfigTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/ConfigTest.php index 5e1dfdc166351..149f6b5e33b6e 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/ConfigTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/ConfigTest.php @@ -15,7 +15,7 @@ class ConfigTest extends \PHPUnit\Framework\TestCase 'es_MX', 'eu_ES', 'es_PE', 'et_EE', 'fa_IR', 'fi_FI', 'fil_PH', 'fr_CA', 'fr_FR', 'gu_IN', 'he_IL', 'hi_IN', 'hr_HR', 'hu_HU', 'id_ID', 'is_IS', 'it_CH', 'it_IT', 'ja_JP', 'ka_GE', 'km_KH', 'ko_KR', 'lo_LA', 'lt_LT', 'lv_LV', 'mk_MK', 'mn_Cyrl_MN', 'ms_Latn_MY', 'nl_NL', 'nb_NO', - 'nn_NO', 'pl_PL', 'pt_BR', 'pt_PT', 'ro_RO', 'ru_RU', 'sk_SK', 'sl_SI', 'sq_AL', 'sr_Cyrl_RS', + 'nn_NO', 'pl_PL', 'pt_BR', 'pt_PT', 'ro_RO', 'ru_RU', 'sk_SK', 'sl_SI', 'sq_AL', 'sr_Cyrl_RS', 'sr_Latn_RS', 'sv_SE', 'sw_KE', 'th_TH', 'tr_TR', 'uk_UA', 'vi_VN', 'zh_Hans_CN', 'zh_Hant_HK', 'zh_Hant_TW', 'es_CL', 'lo_LA', 'es_VE', 'en_IE', ]; diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php index 9e247a8e21ac6..0d51d6fbda305 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/TranslatedListsTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Locale\Test\Unit; @@ -17,23 +18,62 @@ class TranslatedListsTest extends TestCase /** * @var TranslatedLists */ - protected $listsModel; + private $listsModel; /** - * @var MockObject | ConfigInterface + * @var MockObject | ConfigInterface */ - protected $mockConfig; + private $mockConfig; /** - * @var MockObject | ResolverInterface + * @var MockObject | ResolverInterface */ - protected $mockLocaleResolver; + private $mockLocaleResolver; + + /** + * @var array + */ + private $expectedCurrencies = [ + 'USD', + 'EUR', + 'UAH', + 'GBP', + ]; + + /** + * @var array + */ + private $expectedLocales = [ + 'en_US' => 'English (United States)', + 'en_GB' => 'English (United Kingdom)', + 'uk_UA' => 'Ukrainian (Ukraine)', + 'de_DE' => 'German (Germany)', + 'sr_Cyrl_RS' => 'Serbian (Cyrillic, Serbia)', + 'sr_Latn_RS' => 'Serbian (Latin, Serbia)' + ]; + + /** + * @var array + */ + private $expectedTranslatedLocales = [ + 'en_US' => 'English (United States) / English (United States)', + 'en_GB' => 'English (United Kingdom) / English (United Kingdom)', + 'uk_UA' => 'українська (Україна) / Ukrainian (Ukraine)', + 'de_DE' => 'Deutsch (Deutschland) / German (Germany)', + 'sr_Cyrl_RS' => 'српски (ћирилица, Србија) / Serbian (Cyrillic, Serbia)', + 'sr_Latn_RS' => 'Srpski (latinica, Srbija) / Serbian (Latin, Serbia)' + ]; protected function setUp() { $this->mockConfig = $this->getMockBuilder(ConfigInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->mockConfig->method('getAllowedLocales') + ->willReturn(array_keys($this->expectedLocales)); + $this->mockConfig->method('getAllowedCurrencies') + ->willReturn($this->expectedCurrencies); + $this->mockLocaleResolver = $this->getMockBuilder(ResolverInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -69,12 +109,6 @@ public function testGetOptionAllCurrencies() public function testGetOptionCurrencies() { - $allowedCurrencies = ['USD', 'EUR', 'GBP', 'UAH']; - - $this->mockConfig->expects($this->once()) - ->method('getAllowedCurrencies') - ->willReturn($allowedCurrencies); - $expectedResults = ['USD', 'EUR', 'GBP', 'UAH']; $currencyList = $this->listsModel->getOptionCurrencies(); @@ -134,44 +168,34 @@ public function testGetOptionTimezones() public function testGetOptionLocales() { - $this->setupForOptionLocales(); - - $expectedResults = ['en_US', 'uk_UA', 'de_DE']; - - $list = $this->listsModel->getOptionLocales(); - foreach ($expectedResults as $value) { - $found = false; - foreach ($list as $item) { - $found = $found || ($value == $item['value']); - } - $this->assertTrue($found); - } + $locales = array_intersect( + $this->expectedLocales, + $this->convertOptionLocales($this->listsModel->getOptionLocales()) + ); + $this->assertEquals($this->expectedLocales, $locales); } public function testGetTranslatedOptionLocales() { - $this->setupForOptionLocales(); - - $expectedResults = ['en_US', 'uk_UA', 'de_DE']; - - $list = $this->listsModel->getOptionLocales(); - foreach ($expectedResults as $value) { - $found = false; - foreach ($list as $item) { - $found = $found || ($value == $item['value']); - } - $this->assertTrue($found); - } + $locales = array_intersect( + $this->expectedTranslatedLocales, + $this->convertOptionLocales($this->listsModel->getTranslatedOptionLocales()) + ); + $this->assertEquals($this->expectedTranslatedLocales, $locales); } /** - * Setup for option locales + * @param array $optionLocales + * @return array */ - protected function setupForOptionLocales() + private function convertOptionLocales($optionLocales): array { - $allowedLocales = ['en_US', 'uk_UA', 'de_DE']; - $this->mockConfig->expects($this->once()) - ->method('getAllowedLocales') - ->willReturn($allowedLocales); + $result = []; + + foreach ($optionLocales as $optionLocale) { + $result[$optionLocale['value']] = $optionLocale['label']; + } + + return $result; } } diff --git a/lib/internal/Magento/Framework/Locale/TranslatedLists.php b/lib/internal/Magento/Framework/Locale/TranslatedLists.php index 2087564dcec20..e409ca2f03358 100644 --- a/lib/internal/Magento/Framework/Locale/TranslatedLists.php +++ b/lib/internal/Magento/Framework/Locale/TranslatedLists.php @@ -81,17 +81,23 @@ protected function _getOptionLocales($translatedName = false) } $language = \Locale::getPrimaryLanguage($locale); $country = \Locale::getRegion($locale); + $script = \Locale::getScript($locale); + $scriptTranslated = ''; if (!$languages[$language] || !$countries[$country]) { continue; } + if ($script !== '') { + $script = \Locale::getDisplayScript($locale) . ', '; + $scriptTranslated = \Locale::getDisplayScript($locale, $locale) . ', '; + } if ($translatedName) { $label = ucwords(\Locale::getDisplayLanguage($locale, $locale)) - . ' (' . \Locale::getDisplayRegion($locale, $locale) . ') / ' + . ' (' . $scriptTranslated . \Locale::getDisplayRegion($locale, $locale) . ') / ' . $languages[$language] - . ' (' . $countries[$country] . ')'; + . ' (' . $script . $countries[$country] . ')'; } else { $label = $languages[$language] - . ' (' . $countries[$country] . ')'; + . ' (' . $script . $countries[$country] . ')'; } $options[] = ['value' => $locale, 'label' => $label]; } diff --git a/lib/internal/Magento/Framework/Lock/Backend/Database.php b/lib/internal/Magento/Framework/Lock/Backend/Database.php index 096e77a117683..a5a76ba60f4e2 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Database.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Database.php @@ -76,7 +76,7 @@ public function lock(string $name, int $timeout = -1): bool { if (!$this->deploymentConfig->isDbAvailable()) { return true; - }; + } $name = $this->addPrefix($name); /** @@ -117,7 +117,7 @@ public function unlock(string $name): bool { if (!$this->deploymentConfig->isDbAvailable()) { return true; - }; + } $name = $this->addPrefix($name); @@ -145,7 +145,7 @@ public function isLocked(string $name): bool { if (!$this->deploymentConfig->isDbAvailable()) { return false; - }; + } $name = $this->addPrefix($name); diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index 4a8d6572faaf8..0d36bf02f0073 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -377,6 +377,7 @@ protected function prepareMessage() { $template = $this->getTemplate(); $content = $template->processTemplate(); + switch ($template->getType()) { case TemplateTypesInterface::TYPE_TEXT: $part['type'] = MimeInterface::TYPE_TEXT; @@ -391,7 +392,10 @@ protected function prepareMessage() new Phrase('Unknown template type') ); } + + /** @var \Magento\Framework\Mail\MimePartInterface $mimePart */ $mimePart = $this->mimePartInterfaceFactory->create(['content' => $content]); + $this->messageData['encoding'] = $mimePart->getCharset(); $this->messageData['body'] = $this->mimeMessageInterfaceFactory->create( ['parts' => [$mimePart]] ); @@ -400,6 +404,7 @@ protected function prepareMessage() (string)$template->getSubject(), ENT_QUOTES ); + $this->message = $this->emailMessageInterfaceFactory->create($this->messageData); return $this; @@ -427,6 +432,8 @@ private function addAddressByType(string $addressType, $email, ?string $name = n $this->messageData[$addressType], $convertedAddressArray ); + } else { + $this->messageData[$addressType] = $convertedAddressArray; } } } diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php index a40bb9af1e0c4..7c1a947623e9f 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php @@ -5,14 +5,13 @@ */ namespace Magento\Framework\MessageQueue; -use Doctrine\Instantiator\Exception\InvalidArgumentException; +use InvalidArgumentException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Phrase; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; /** - * Class MessageValidator to validate message with topic schema - * + * Class MessageValidator to validate message with topic schema. */ class MessageValidator { @@ -58,6 +57,7 @@ protected function getTopicSchema($topic, $requestType) * @param bool $requestType * @return void * @throws InvalidArgumentException + * @throws LocalizedException */ public function validate($topic, $message, $requestType = true) { @@ -89,6 +89,8 @@ public function validate($topic, $message, $requestType = true) } /** + * Validate queue message. + * * @param string $message * @param string $messageType * @param string $topic @@ -104,6 +106,8 @@ protected function validateMessage($message, $messageType, $topic) } /** + * Validate message primitive type. + * * @param string $message * @param string $messageType * @param string $topic @@ -135,6 +139,8 @@ protected function validatePrimitiveType($message, $messageType, $topic) } /** + * Validate class type + * * @param string $message * @param string $messageType * @param string $topic @@ -167,6 +173,8 @@ protected function validateClassType($message, $messageType, $topic) } /** + * Returns message real type + * * @param string $message * @return string */ diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 949e002a14208..69410b7757e44 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -362,6 +362,9 @@ private function populateExtensionAttributes(array $extensionAttributesData = [] /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -370,6 +373,9 @@ public function __sleep() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index 8018c6176390f..534c25fce8d42 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -220,6 +220,9 @@ protected function _init($resourceModel) * Remove unneeded properties from serialization * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -244,6 +247,9 @@ public function __sleep() * Init not serializable fields * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php index fc0edf931fa9c..0b44dc60c6504 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php @@ -157,6 +157,9 @@ public function __construct( * Provide variables to serialize * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -169,6 +172,9 @@ public function __sleep() * Restore global dependencies * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { @@ -220,9 +226,10 @@ protected function _setResource($connections, $tables = null) } /** - * Set main entity table name and primary key field name + * Main table setter. * - * If field name is omitted {table_name}_id will be used + * Set main entity table name and primary key field name. + * If field name is omitted {table_name}_id will be used. * * @param string $mainTable * @param string|null $idFieldName @@ -255,7 +262,10 @@ public function getIdFieldName() } /** - * Returns main table name - extracted from "module/table" style and validated by db adapter + * Main table getter. + * + * Returns main table name - extracted from "module/table" style and + * validated by db adapter. * * @throws LocalizedException * @return string @@ -544,7 +554,7 @@ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $o } /** - * Check that model data fields that can be saved has really changed comparing with origData + * Check that model data fields that can be saved has really changed comparing with origData. * * @param \Magento\Framework\Model\AbstractModel $object * @return bool @@ -784,6 +794,24 @@ protected function saveNewObject(\Magento\Framework\Model\AbstractModel $object) } } + /** + * Check if column data type is numeric + * + * Based on column description + * + * @param array $columnDescription + * @return bool + */ + private function isNumericValue(array $columnDescription): bool + { + $result = true; + if (!empty($columnDescription['DATA_TYPE']) + && in_array($columnDescription['DATA_TYPE'], ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'])) { + $result = false; + } + return $result; + } + /** * Update existing object * @@ -793,29 +821,35 @@ protected function saveNewObject(\Magento\Framework\Model\AbstractModel $object) */ protected function updateObject(\Magento\Framework\Model\AbstractModel $object) { - $condition = $this->getConnection()->quoteInto($this->getIdFieldName() . '=?', $object->getId()); + $connection = $this->getConnection(); + $tableDescription = $connection->describeTable($this->getMainTable()); + $preparedValue = $connection->prepareColumnValue($tableDescription[$this->getIdFieldName()], $object->getId()); + $condition = (!$this->isNumericValue($tableDescription[$this->getIdFieldName()])) + ? sprintf('%s=%d', $this->getIdFieldName(), $preparedValue) + : $connection->quoteInto($this->getIdFieldName() . '=?', $preparedValue); + /** * Not auto increment primary key support */ if ($this->_isPkAutoIncrement) { $data = $this->prepareDataForUpdate($object); if (!empty($data)) { - $this->getConnection()->update($this->getMainTable(), $data, $condition); + $connection->update($this->getMainTable(), $data, $condition); } } else { - $select = $this->getConnection()->select()->from( + $select = $connection->select()->from( $this->getMainTable(), [$this->getIdFieldName()] )->where( $condition ); - if ($this->getConnection()->fetchOne($select) !== false) { + if ($connection->fetchOne($select) !== false) { $data = $this->prepareDataForUpdate($object); if (!empty($data)) { - $this->getConnection()->update($this->getMainTable(), $data, $condition); + $connection->update($this->getMainTable(), $data, $condition); } } else { - $this->getConnection()->insert( + $connection->insert( $this->getMainTable(), $this->_prepareDataForSave($object) ); diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php index cba5f133f53c8..1186326ab6525 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php @@ -607,6 +607,9 @@ public function save() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -619,6 +622,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/AbstractDbTest.php b/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/AbstractDbTest.php index b69f50cf4f341..2a87cd774332a 100644 --- a/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/AbstractDbTest.php +++ b/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/AbstractDbTest.php @@ -425,16 +425,15 @@ public function testPrepareDataForUpdate() $connectionMock = $this->getMockBuilder(AdapterInterface::class) ->setMethods(['save']) ->getMockForAbstractClass(); + $context = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( \Magento\Framework\Model\Context::class ); $registryMock = $this->createMock(\Magento\Framework\Registry::class); - $resourceMock = $this->createPartialMock(AbstractDb::class, [ - '_construct', - 'getConnection', - '__wakeup', - 'getIdFieldName' - ]); + $resourceMock = $this->createPartialMock( + AbstractDb::class, + ['_construct', 'getConnection', '__wakeup', 'getIdFieldName'] + ); $connectionInterfaceMock = $this->createMock(AdapterInterface::class); $resourceMock->expects($this->any()) ->method('getConnection') @@ -453,6 +452,7 @@ public function testPrepareDataForUpdate() $this->_resourcesMock->expects($this->any())->method('getTableName')->with($data)->will( $this->returnValue('tableName') ); + $mainTableReflection = new \ReflectionProperty( AbstractDb::class, '_mainTable' @@ -467,6 +467,13 @@ public function testPrepareDataForUpdate() $idFieldNameReflection->setValue($this->_model, 'idFieldName'); $connectionMock->expects($this->any())->method('save')->with('tableName', 'idFieldName'); $connectionMock->expects($this->any())->method('quoteInto')->will($this->returnValue('idFieldName')); + $connectionMock->expects($this->any()) + ->method('describeTable') + ->with('tableName') + ->willReturn(['idFieldName' => []]); + $connectionMock->expects($this->any()) + ->method('prepareColumnValue') + ->willReturn(0); $abstractModelMock->setIdFieldName('id'); $abstractModelMock->setData( [ diff --git a/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php index 470ba16bdd40c..cfa79f3e7ee60 100644 --- a/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php @@ -55,9 +55,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -68,6 +71,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php index c867dced0fc6e..f6bf61708dd80 100644 --- a/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php +++ b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php @@ -17,7 +17,7 @@ class JsonHexTagTest extends \PHPUnit\Framework\TestCase * @var \Magento\Framework\Serialize\Serializer\Json */ private $json; - + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -37,6 +37,9 @@ public function testSerialize($value, $expected) ); } + /** + * @return array + */ public function serializeDataProvider() { $dataObject = new DataObject(['something']); diff --git a/lib/internal/Magento/Framework/Setup/Lists.php b/lib/internal/Magento/Framework/Setup/Lists.php index 1ee5baf28658e..98c9b6cc0a4b4 100644 --- a/lib/internal/Magento/Framework/Setup/Lists.php +++ b/lib/internal/Magento/Framework/Setup/Lists.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Setup; @@ -12,6 +13,9 @@ use Magento\Framework\Locale\ConfigInterface; use Magento\Framework\Locale\Resolver; +/** + * Retrieves lists of allowed locales and currencies + */ class Lists { /** @@ -99,10 +103,14 @@ public function getLocaleList() } $language = \Locale::getPrimaryLanguage($locale); $country = \Locale::getRegion($locale); + $script = \Locale::getScript($locale); if (!$languages[$language] || !$countries[$country]) { continue; } - $list[$locale] = $languages[$language] . ' (' . $countries[$country] . ')'; + if ($script !== '') { + $script = \Locale::getDisplayScript($locale) . ', '; + } + $list[$locale] = $languages[$language] . ' (' . $script . $countries[$country] . ')'; } asort($list); return $list; diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/ListsTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/ListsTest.php index a25771b4519f2..c9c6dfe6d7dc2 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/ListsTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/ListsTest.php @@ -3,27 +3,31 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Setup\Test\Unit; +use Magento\Framework\Locale\ConfigInterface; use Magento\Framework\Setup\Lists; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; -class ListsTest extends \PHPUnit\Framework\TestCase +class ListsTest extends TestCase { /** * @var Lists */ - protected $lists; + private $lists; /** - * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Locale\ConfigInterface + * @var MockObject|ConfigInterface */ - protected $mockConfig; + private $mockConfig; /** * @var array */ - protected $expectedTimezones = [ + private $expectedTimezones = [ 'Australia/Darwin', 'America/Los_Angeles', 'Europe/Kiev', @@ -33,7 +37,7 @@ class ListsTest extends \PHPUnit\Framework\TestCase /** * @var array */ - protected $expectedCurrencies = [ + private $expectedCurrencies = [ 'USD', 'EUR', 'UAH', @@ -43,23 +47,23 @@ class ListsTest extends \PHPUnit\Framework\TestCase /** * @var array */ - protected $expectedLocales = [ - 'en_US', - 'en_GB', - 'uk_UA', - 'de_DE', + private $expectedLocales = [ + 'en_US' => 'English (United States)', + 'en_GB' => 'English (United Kingdom)', + 'uk_UA' => 'Ukrainian (Ukraine)', + 'de_DE' => 'German (Germany)', + 'sr_Cyrl_RS' => 'Serbian (Cyrillic, Serbia)', + 'sr_Latn_RS' => 'Serbian (Latin, Serbia)' ]; protected function setUp() { - $this->mockConfig = $this->getMockBuilder(\Magento\Framework\Locale\ConfigInterface::class) + $this->mockConfig = $this->getMockBuilder(ConfigInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->mockConfig->expects($this->any()) - ->method('getAllowedLocales') - ->willReturn($this->expectedLocales); - $this->mockConfig->expects($this->any()) - ->method('getAllowedCurrencies') + $this->mockConfig->method('getAllowedLocales') + ->willReturn(array_keys($this->expectedLocales)); + $this->mockConfig->method('getAllowedCurrencies') ->willReturn($this->expectedCurrencies); $this->lists = new Lists($this->mockConfig); @@ -73,13 +77,10 @@ public function testGetTimezoneList() public function testGetLocaleList() { - $locales = array_intersect($this->expectedLocales, array_keys($this->lists->getLocaleList())); + $locales = array_intersect($this->expectedLocales, $this->lists->getLocaleList()); $this->assertEquals($this->expectedLocales, $locales); } - /** - * Test Lists:getCurrencyList() considering allowed currencies config values. - */ public function testGetCurrencyList() { $currencies = array_intersect($this->expectedCurrencies, array_keys($this->lists->getCurrencyList())); diff --git a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php index d2b0468bebde9..62b3352e11b1a 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php +++ b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php @@ -55,9 +55,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to other objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -68,6 +71,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Url.php b/lib/internal/Magento/Framework/Url.php index 11aeb1c0c79b8..c67a20f0a157d 100644 --- a/lib/internal/Magento/Framework/Url.php +++ b/lib/internal/Magento/Framework/Url.php @@ -62,6 +62,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Url extends \Magento\Framework\DataObject implements \Magento\Framework\UrlInterface { @@ -675,8 +676,7 @@ protected function _getControllerName($default = null) } /** - * Set Action name - * Reseted route path if action name has change + * Set Action name, reseated route path if action name has change * * @param string $data * @return \Magento\Framework\UrlInterface @@ -1067,7 +1067,7 @@ public function sessionUrlVar($html) */ // @codingStandardsIgnoreEnd function ($match) { - if ($this->useSessionIdForUrl($match[2] == 'S' ? true : false)) { + if ($this->useSessionIdForUrl($match[2] == 'S')) { return $match[1] . $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId() . (isset($match[3]) ? $match[3] : ''); } else { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSource.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSource.php index 7016bbdb08ab2..b05a12deb3843 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSource.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSource.php @@ -6,9 +6,9 @@ namespace Magento\Framework\View\Asset\PreProcessor; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Asset\ContentProcessorInterface; use Magento\Framework\View\Asset\File\FallbackContext; use Magento\Framework\View\Asset\LockerProcessInterface; -use Magento\Framework\View\Asset\ContentProcessorInterface; use Magento\Framework\View\Asset\PreProcessor\AlternativeSource\AssetBuilder; /** @@ -139,11 +139,13 @@ private function processContent($path, $content, $module, FallbackContext $conte ->setTheme($context->getThemePath()) ->setLocale($context->getLocale()) ->setModule($module) - ->setPath(preg_replace( - '#\.' . preg_quote(pathinfo($path, PATHINFO_EXTENSION)) . '$#', - '.' . $name, - $path - ))->build(); + ->setPath( + preg_replace( + '#\.' . preg_quote(pathinfo($path, PATHINFO_EXTENSION)) . '$#', + '.' . $name, + $path + ) + )->build(); $processor = $this->objectManager->get($alternative[self::PROCESSOR_CLASS]); if (!$processor instanceof ContentProcessorInterface) { @@ -168,4 +170,15 @@ public function getAlternativesExtensionsNames() { return array_keys($this->alternatives); } + + /** + * Check if file extension supported + * + * @param string $ext + * @return bool + */ + public function isExtensionSupported($ext) + { + return isset($this->alternatives[$ext]); + } } diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/FileNameResolver.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/FileNameResolver.php index 2afb97b918ea1..bae2ff53a54e6 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/FileNameResolver.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/FileNameResolver.php @@ -5,6 +5,11 @@ */ namespace Magento\Framework\View\Asset\PreProcessor; +/** + * Class FileNameResolver + * + * @package Magento\Framework\View\Asset\PreProcessor + */ class FileNameResolver { /** @@ -38,7 +43,7 @@ public function resolve($fileName) $compiledFile = $fileName; $extension = pathinfo($fileName, PATHINFO_EXTENSION); foreach ($this->alternativeSources as $name => $alternative) { - if (in_array($extension, $alternative->getAlternativesExtensionsNames(), true) + if ($alternative->isExtensionSupported($extension) && strpos(basename($fileName), '_') !== 0 ) { $compiledFile = substr($fileName, 0, strlen($fileName) - strlen($extension) - 1); diff --git a/lib/internal/Magento/Framework/View/Layout/Proxy.php b/lib/internal/Magento/Framework/View/Layout/Proxy.php index ec5ce761154ed..2ee50f8d14bc3 100644 --- a/lib/internal/Magento/Framework/View/Layout/Proxy.php +++ b/lib/internal/Magento/Framework/View/Layout/Proxy.php @@ -57,9 +57,12 @@ public function __construct( } /** - * Sleep magic method. + * Remove links to objects. * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { @@ -70,6 +73,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index af2eb913fe3fe..dfbfb5a25debe 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -19,7 +19,6 @@ "ext-intl": "*", "ext-openssl": "*", "ext-simplexml": "*", - "ext-spl": "*", "ext-xsl": "*", "ext-bcmath": "*", "lib-libxml": "*", diff --git a/lib/web/css/docs/layout.html b/lib/web/css/docs/layout.html index 77ed0597f0748..b338c66ffae71 100644 --- a/lib/web/css/docs/layout.html +++ b/lib/web/css/docs/layout.html @@ -55,25 +55,25 @@ <tr> <th>@layout-class-1column</th> <td class="vars_value">page-layout-1column</td> - <td class="vars_value">'' | false | <nobr>page-layout-1column</nobr> | <nobr>page-layout-2columns-left</nobr> | <nobr>page-layout-2columns-right</nobr> | <nobr>page-layout-3columns</nobr></td> + <td class="vars_value">'' | false | <span style="white-space: nowrap">page-layout-1column</span> | <span style="white-space: nowrap">page-layout-2columns-left</span> | <span style="white-space: nowrap">page-layout-2columns-right</span> | <span style="white-space: nowrap">page-layout-3columns</span></td> <td>Class name for one column layout</td> </tr> <tr> <th>@layout-class-2columns__left</th> <td class="vars_value">page-layout-2columns-left</td> - <td class="vars_value">'' | false | <nobr>page-layout-1column</nobr> | <nobr>page-layout-2columns-left</nobr> | <nobr>page-layout-2columns-right</nobr> | <nobr>page-layout-3columns</nobr></td> + <td class="vars_value">'' | false | <span style="white-space: nowrap">page-layout-1column</span> | <span style="white-space: nowrap">page-layout-2columns-left</span> | <span style="white-space: nowrap">page-layout-2columns-right</span> | <span style="white-space: nowrap">page-layout-3columns</span></td> <td>Class name for two columns layout with left sidebar</td> </tr> <tr> <th nowrap="nowrap">@layout-class-2columns__right</th> <td class="vars_value">page-layout-2columns-right</td> - <td class="vars_value">'' | false | <nobr>page-layout-1column</nobr> | <nobr>page-layout-2columns-left</nobr> | <nobr>page-layout-2columns-right</nobr> | <nobr>page-layout-3columns</nobr></td> + <td class="vars_value">'' | false | <span style="white-space: nowrap">page-layout-1column</span> | <span style="white-space: nowrap">page-layout-2columns-left</span> | <span style="white-space: nowrap">page-layout-2columns-right</span> | <span style="white-space: nowrap">page-layout-3columns</span></td> <td>Class name for two columns layout with right sidebar</td> </tr> <tr> <th>@layout-class-3columns</th> <td class="vars_value">page-layout-3columns</td> - <td class="vars_value">'' | false | <nobr>page-layout-1column</nobr> | <nobr>page-layout-2columns-left</nobr> | <nobr>page-layout-2columns-right</nobr> | <nobr>page-layout-3columns</nobr></td> + <td class="vars_value">'' | false | <span style="white-space: nowrap">page-layout-1column</span> | <span style="white-space: nowrap">page-layout-2columns-left</span> | <span style="white-space: nowrap">page-layout-2columns-right</span> | <span style="white-space: nowrap">page-layout-3columns</span></td> <td>Class name for three columns layout with left sidebar</td> </tr> <tr> diff --git a/lib/web/css/docs/responsive.html b/lib/web/css/docs/responsive.html index 48d0bd551bd92..ebc42e698f60b 100644 --- a/lib/web/css/docs/responsive.html +++ b/lib/web/css/docs/responsive.html @@ -7,7 +7,7 @@ <!DOCTYPE html><html><head><title>responsive | Magento UI Library

Responsive

-

Magento UI library provides a strong approach for working with media queries. It`s based on recursive call of .media-width() mixin defined anywhere in project but invoked in one place in lib/web/css/source/lib/_responsive.less. That's why in the resulting styles.css we have every media query only once with all the rules there, not a multiple calls for the same query.

+

Magento UI library provides a strong approach for working with media queries. It`s based on recursive call of .media-width() mixin defined anywhere in project but invoked in one place in lib/web/css/source/lib/_responsive.less. That's why in the resulting styles.css we have every media query only once with all the rules there, not a multiple calls for the same query.

To see the media queries work resize window to understand which breakpoint is applied.