diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 3f908663c8e5e..a247e6b09760b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -916,7 +916,12 @@ public function addWebsiteFilter($websites = null) } $this->_productLimitationFilters['website_ids'] = $websites; - $this->_applyProductLimitations(); + + if ($this->getStoreId() == Store::DEFAULT_STORE_ID) { + $this->_productLimitationJoinWebsite(); + } else { + $this->_applyProductLimitations(); + } return $this; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php index 2bf504369b8a7..9a55e48cfb1b4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php @@ -38,6 +38,7 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Validator\UniversalFactory; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -93,6 +94,11 @@ class CollectionTest extends TestCase */ private $storeManager; + /** + * @var ProductLimitation|MockObject + */ + private $productLimitationMock; + /** * @var EntityFactory|MockObject */ @@ -192,7 +198,7 @@ protected function setUp(): void $this->entityMock->expects($this->any())->method('getTable')->willReturnArgument(0); $this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock); - $productLimitationMock = $this->createMock( + $this->productLimitationMock = $this->createMock( ProductLimitation::class ); $productLimitationFactoryMock = $this->getMockBuilder( @@ -201,7 +207,7 @@ protected function setUp(): void ->setMethods(['create'])->getMock(); $productLimitationFactoryMock->method('create') - ->willReturn($productLimitationMock); + ->willReturn($this->productLimitationMock); $this->collection = $this->objectManager->getObject( Collection::class, [ @@ -432,4 +438,44 @@ public function testGetNewEmptyItem() $secondItem = $this->collection->getNewEmptyItem(); $this->assertEquals($firstItem, $secondItem); } + + /** + * Test to add website filter in admin area + */ + public function testAddWebsiteFilterOnAdminStore(): void + { + $websiteIds = [2]; + $websiteTable = 'catalog_product_website'; + $joinCondition = 'join condition'; + $this->productLimitationMock->expects($this->atLeastOnce()) + ->method('offsetSet') + ->with('website_ids', $websiteIds); + $this->productLimitationMock->method('offsetExists') + ->with('website_ids') + ->willReturn(true); + $this->productLimitationMock->method('offsetGet') + ->with('website_ids') + ->willReturn($websiteIds); + $this->connectionMock->expects($this->once()) + ->method('quoteInto') + ->with('product_website.website_id IN(?)', $websiteIds, 'int') + ->willReturn($joinCondition); + $this->selectMock->method('getPart')->with(Select::FROM)->willReturn([]); + /** @var AbstractEntity|MockObject $eavEntity */ + $eavEntity = $this->createMock(AbstractEntity::class); + $eavEntity->method('getTable') + ->with('catalog_product_website') + ->willReturn($websiteTable); + $this->selectMock->expects($this->once()) + ->method('join') + ->with( + ['product_website' => $websiteTable], + 'product_website.product_id = e.entity_id AND ' . $joinCondition, + [] + ); + + $this->collection->setEntity($eavEntity); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); + $this->collection->addWebsiteFilter($websiteIds); + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php index 22bbc991a78e2..8218c22b8056d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php @@ -132,7 +132,7 @@ private function fetch() : array $this->searchCriteriaBuilder->create(), $this->attributeCodes, false, - true + false ); /** @var \Magento\Catalog\Model\Product $product */ diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 3e955ae303453..30be41072242b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -89,7 +89,7 @@ public function getList( $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context); - if (!$isChildSearch) { + if ($isChildSearch) { $visibilityIds = $isSearch ? $this->visibility->getVisibleInSearchIds() : $this->visibility->getVisibleInCatalogIds(); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php index 22a83671f630a..84cdc4608148c 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php @@ -220,7 +220,7 @@ private function deleteProductsLinks( if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $importEntity->getBehavior()) { foreach ($linksToDelete as $linkTypeId => $productIds) { if (!empty($productIds)) { - $whereLinkId = $importEntity->getConnection()->quoteInto('link_type_id', $linkTypeId); + $whereLinkId = $importEntity->getConnection()->quoteInto('link_type_id = ?', $linkTypeId); $whereProductId = $importEntity->getConnection()->quoteInto( 'product_id IN (?)', array_unique($productIds) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php index 944710773123f..3e2301f34c4f8 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php @@ -6,8 +6,8 @@ namespace Magento\CatalogRule\Model\Indexer; -use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\CatalogRule\Model\Rule; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; @@ -18,6 +18,8 @@ */ class ReindexRuleProduct { + private const ADMIN_WEBSITE_ID = 0; + /** * @var ResourceConnection */ @@ -38,22 +40,30 @@ class ReindexRuleProduct */ private $localeDate; + /** + * @var bool + */ + private $useWebsiteTimezone; + /** * @param ResourceConnection $resource * @param ActiveTableSwitcher $activeTableSwitcher * @param TableSwapper $tableSwapper * @param TimezoneInterface $localeDate + * @param bool $useWebsiteTimezone */ public function __construct( ResourceConnection $resource, ActiveTableSwitcher $activeTableSwitcher, TableSwapper $tableSwapper, - TimezoneInterface $localeDate + TimezoneInterface $localeDate, + bool $useWebsiteTimezone = true ) { $this->resource = $resource; $this->activeTableSwitcher = $activeTableSwitcher; $this->tableSwapper = $tableSwapper; $this->localeDate = $localeDate; + $this->useWebsiteTimezone = $useWebsiteTimezone; } /** @@ -95,18 +105,18 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) $actionOperator = $rule->getSimpleAction(); $actionAmount = $rule->getDiscountAmount(); $actionStop = $rule->getStopRulesProcessing(); + $fromTimeInAdminTz = $this->parseDateByWebsiteTz((string)$rule->getFromDate(), self::ADMIN_WEBSITE_ID); + $toTimeInAdminTz = $this->parseDateByWebsiteTz((string)$rule->getToDate(), self::ADMIN_WEBSITE_ID); $rows = []; foreach ($websiteIds as $websiteId) { - $scopeTz = new \DateTimeZone( - $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId) - ); - $fromTime = $rule->getFromDate() - ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp() - : 0; - $toTime = $rule->getToDate() - ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1 - : 0; + $fromTime = $this->useWebsiteTimezone + ? $this->parseDateByWebsiteTz((string)$rule->getFromDate(), (int)$websiteId) + : $fromTimeInAdminTz; + $toTime = $this->useWebsiteTimezone + ? $this->parseDateByWebsiteTz((string)$rule->getToDate(), (int)$websiteId) + + ($rule->getToDate() ? IndexBuilder::SECONDS_IN_DAY - 1 : 0) + : $toTimeInAdminTz; foreach ($productIds as $productId => $validationByWebsite) { if (empty($validationByWebsite[$websiteId])) { @@ -140,4 +150,23 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) return true; } + + /** + * Parse date value by the timezone of the website + * + * @param string $date + * @param int $websiteId + * @return int + */ + private function parseDateByWebsiteTz(string $date, int $websiteId): int + { + if (empty($date)) { + return 0; + } + + $websiteTz = $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId); + $dateTime = new \DateTime($date, new \DateTimeZone($websiteTz)); + + return $dateTime->getTimestamp(); + } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php index 51869f1accbb3..ccc5352567ff0 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php @@ -7,6 +7,7 @@ namespace Magento\CatalogRule\Model\Indexer; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; /** @@ -39,25 +40,33 @@ class ReindexRuleProductPrice */ private $pricesPersistor; + /** + * @var bool + */ + private $useWebsiteTimezone; + /** * @param StoreManagerInterface $storeManager * @param RuleProductsSelectBuilder $ruleProductsSelectBuilder * @param ProductPriceCalculator $productPriceCalculator * @param TimezoneInterface $localeDate * @param RuleProductPricesPersistor $pricesPersistor + * @param bool $useWebsiteTimezone */ public function __construct( StoreManagerInterface $storeManager, RuleProductsSelectBuilder $ruleProductsSelectBuilder, ProductPriceCalculator $productPriceCalculator, TimezoneInterface $localeDate, - RuleProductPricesPersistor $pricesPersistor + RuleProductPricesPersistor $pricesPersistor, + bool $useWebsiteTimezone = true ) { $this->storeManager = $storeManager; $this->ruleProductsSelectBuilder = $ruleProductsSelectBuilder; $this->productPriceCalculator = $productPriceCalculator; $this->localeDate = $localeDate; $this->pricesPersistor = $pricesPersistor; + $this->useWebsiteTimezone = $useWebsiteTimezone; } /** @@ -82,11 +91,9 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi $prevKey = null; $storeGroup = $this->storeManager->getGroup($website->getDefaultGroupId()); - $currentDate = $this->localeDate->scopeDate($storeGroup->getDefaultStoreId(), null, true); - $previousDate = (clone $currentDate)->modify('-1 day'); - $previousDate->setTime(23, 59, 59); - $nextDate = (clone $currentDate)->modify('+1 day'); - $nextDate->setTime(0, 0, 0); + $dateInterval = $this->useWebsiteTimezone + ? $this->getDateInterval((int)$storeGroup->getDefaultStoreId()) + : $this->getDateInterval(Store::DEFAULT_STORE_ID); while ($ruleData = $productsStmt->fetch()) { $ruleProductId = $ruleData['product_id']; @@ -107,7 +114,7 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi /** * Build prices for each day */ - foreach ([$previousDate, $currentDate, $nextDate] as $date) { + foreach ($dateInterval as $date) { $time = $date->getTimestamp(); if (($ruleData['from_time'] == 0 || $time >= $ruleData['from_time']) && ($ruleData['to_time'] == 0 || @@ -157,4 +164,21 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi return true; } + + /** + * Retrieve date sequence in store time zone + * + * @param int $storeId + * @return \DateTime[] + */ + private function getDateInterval(int $storeId): array + { + $currentDate = $this->localeDate->scopeDate($storeId, null, true); + $previousDate = (clone $currentDate)->modify('-1 day'); + $previousDate->setTime(23, 59, 59); + $nextDate = (clone $currentDate)->modify('+1 day'); + $nextDate->setTime(0, 0, 0); + + return [$previousDate, $currentDate, $nextDate]; + } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..cf8499ca1b466 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + Clicks on Save and Continue Edit. Validates that the Success Message is present and correct on the Admin Catalog Price Rule creation/edit page. + + + + + + + diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php index 7f22634f9343d..230a6e3860950 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php @@ -64,7 +64,8 @@ protected function setUp(): void $this->ruleProductsSelectBuilderMock, $this->productPriceCalculatorMock, $this->localeDate, - $this->pricesPersistorMock + $this->pricesPersistorMock, + true ); } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php index ddb6a85ed614a..50f4eb0805ed2 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php @@ -21,6 +21,8 @@ class ReindexRuleProductTest extends TestCase { + private const ADMIN_WEBSITE_ID = 0; + /** * @var ReindexRuleProduct */ @@ -57,7 +59,8 @@ protected function setUp(): void $this->resourceMock, $this->activeTableSwitcherMock, $this->tableSwapperMock, - $this->localeDateMock + $this->localeDateMock, + true ); } @@ -85,6 +88,7 @@ public function testExecuteIfRuleWithoutWebsiteIds() public function testExecute() { $websiteId = 3; + $adminTimeZone = 'America/Chicago'; $websiteTz = 'America/Los_Angeles'; $productIds = [ 4 => [$websiteId => 1], @@ -123,10 +127,11 @@ public function testExecute() $ruleMock->expects($this->once())->method('getDiscountAmount')->willReturn(43); $ruleMock->expects($this->once())->method('getStopRulesProcessing')->willReturn(true); - $this->localeDateMock->expects($this->once()) - ->method('getConfigTimezone') - ->with(ScopeInterface::SCOPE_WEBSITE, $websiteId) - ->willReturn($websiteTz); + $this->localeDateMock->method('getConfigTimezone') + ->willReturnMap([ + [ScopeInterface::SCOPE_WEBSITE, self::ADMIN_WEBSITE_ID, $adminTimeZone], + [ScopeInterface::SCOPE_WEBSITE, $websiteId, $websiteTz], + ]); $batchRows = [ [ diff --git a/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertProductVideoNavigationArrowsActionGroup.xml b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertProductVideoNavigationArrowsActionGroup.xml new file mode 100644 index 0000000000000..d0673dfc1adb7 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/ActionGroup/AssertProductVideoNavigationArrowsActionGroup.xml @@ -0,0 +1,42 @@ + + + + + + + + Validates the navigation arrows on the Storefront Product page. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index e94d426ba7638..44c65c197f7db 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -14,5 +14,7 @@ + + diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/VimeoVideoControlButtonsOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/VimeoVideoControlButtonsOnProductPageTest.xml new file mode 100644 index 0000000000000..a60a5526498ce --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/VimeoVideoControlButtonsOnProductPageTest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + <description value="Navigation arrow buttons not visible after video starts on product image"/> + <severity value="MAJOR"/> + <testCaseId value="MC-40398"/> + <useCaseId value="MC-39759"/> + <group value="productVideo"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Login to Admin page --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!-- Logout from Admin page --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Open product edit page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <!-- Add image to product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <!-- Add product video --> + <actionGroup ref="AddProductVideoActionGroup" stepKey="addProductVideo"> + <argument name="video" value="VimeoProductVideo"/> + </actionGroup> + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> + + <!-- Open storefront product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Check the navigation arrows on Storefront Product page --> + <actionGroup ref="AssertProductVideoNavigationArrowsActionGroup" stepKey="assertProductVideoNavigationArrowsOnStorefrontProductPage"> + <argument name="videoType" value="vimeo"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index acaf2afeb6c26..bfd685543d7f7 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -714,6 +714,7 @@ define([ } $('.' + this.FTAR).addClass(this.isFullscreen ? 'fotorama__arr--shown' : 'fotorama__arr--hidden'); + $('.' + this.FTVC).addClass('fotorama-show-control'); } }, diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php index 82c612c1a781d..094fac313d398 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php @@ -5,16 +5,25 @@ */ namespace Magento\Sales\Model\ResourceModel\Order\Grid; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; +use Magento\Sales\Model\ResourceModel\Order; use Psr\Log\LoggerInterface as Logger; /** * Order grid collection */ -class Collection extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult +class Collection extends SearchResult { + /** + * @var TimezoneInterface + */ + private $timeZone; + /** * Initialize dependencies. * @@ -24,6 +33,7 @@ class Collection extends \Magento\Framework\View\Element\UiComponent\DataProvide * @param EventManager $eventManager * @param string $mainTable * @param string $resourceModel + * @param TimezoneInterface|null $timeZone */ public function __construct( EntityFactory $entityFactory, @@ -31,9 +41,12 @@ public function __construct( FetchStrategy $fetchStrategy, EventManager $eventManager, $mainTable = 'sales_order_grid', - $resourceModel = \Magento\Sales\Model\ResourceModel\Order::class + $resourceModel = Order::class, + TimezoneInterface $timeZone = null ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel); + $this->timeZone = $timeZone ?: ObjectManager::getInstance() + ->get(TimezoneInterface::class); } /** @@ -50,4 +63,20 @@ protected function _initSelect() return $this; } + + /** + * @inheritDoc + */ + public function addFieldToFilter($field, $condition = null) + { + if ($field === 'created_at') { + if (is_array($condition)) { + foreach ($condition as $key => $value) { + $condition[$key] = $this->timeZone->convertConfigTimeToUtc($value); + } + } + } + + return parent::addFieldToFilter($field, $condition); + } } diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontAssertDropDownSearchSuggestionActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontAssertDropDownSearchSuggestionActionGroup.xml new file mode 100644 index 0000000000000..226e30486251c --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontAssertDropDownSearchSuggestionActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StoreFrontAssertDropDownSearchSuggestionActionGroup"> + <annotations> + <description>Fills the Storefront Quick Search field. Validates that the Search Suggestion is present</description> + </annotations> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" stepKey="waitForQuickSearchToBeVisible"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchInput"/> + <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchDropDownSuggestion}}" stepKey="WaitForSearchDropDownSuggestion"/> + <click selector="//div[@class='panel wrapper']" stepKey="clickOnSomewhere"/> + <dontSee selector="{{StorefrontQuickSearchSection.searchDropDownSuggestion}}" stepKey="dontSeeDropDownSuggestion"/> + <click selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="clickOnSearchPhrase"/> + <pressKey selector="{{StorefrontQuickSearchSection.searchPhrase}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::DOWN]" stepKey="pressDown"/> + <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchDropDownSuggestion}}" stepKey="WaitForSearchDropDownSuggestionSecond"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml new file mode 100644 index 0000000000000..b9c491705316c --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySearchSuggestionByControlButtonsTest"> + <annotations> + <stories value="Search Term"/> + <title value="Auto suggestion box not reappearing after clicking outside the text field"/> + <description value="Auto suggestion box not reappearing after clicking outside the text field"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-40466"/> + <useCaseId value="MC-40376"/> + </annotations> + + <before> + <!-- Create Simple Product --> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Perform reindex and flush cache --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <!-- Delete create product --> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + + <!-- Go to the catalog search term page --> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> + + <!-- Filter the search term --> + <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> + <argument name="searchQuery" value="$simpleProduct.name$"/> + </actionGroup> + <!-- Delete created below search terms --> + <actionGroup ref="AdminDeleteSearchTermActionGroup" stepKey="deleteSearchTerms"/> + </after> + + <!-- Go to storefront home page --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + + <!-- Storefront quick search by product name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductName"> + <argument name="phrase" value="$simpleProduct.name$"/> + </actionGroup> + + <!-- Verify search suggestions and select the suggestion from dropdown options --> + <actionGroup ref="StoreFrontAssertDropDownSearchSuggestionActionGroup" stepKey="seeDropDownSearchSuggestion"> + <argument name="searchQuery" value="$simpleProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index b8034fead76d0..df651feb89d45 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -253,6 +253,8 @@ define([ } this.element.val(this.responseList.selected.find('.qs-option-name').text()); this.element.attr('aria-activedescendant', this.responseList.selected.attr('id')); + this._updateAriaHasPopup(true); + this.autoComplete.show(); } break; @@ -269,6 +271,8 @@ define([ } this.element.val(this.responseList.selected.find('.qs-option-name').text()); this.element.attr('aria-activedescendant', this.responseList.selected.attr('id')); + this._updateAriaHasPopup(true); + this.autoComplete.show(); } break; default: diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index f2cf90c95de18..8575f1d33c435 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -190,4 +190,40 @@ private function assertRelatedProducts(array $relatedProducts): void self::assertEquals($product['url_key'], $productExpectedData['url_key']); } } + + /** + * Test query with disabled linked product in the default store + * + * @magentoApiDataFixture Magento/Catalog/_files/products_related_disabled_in_store.php + * + * @return void + */ + public function testQueryDisableRelatedProductInStore(): void + { + $productSku = 'simple_with_related'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + related_products + { + sku + name + url_key + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', ['Store' => 'default']); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('related_products', $response['products']['items'][0]); + $relatedProducts = $response['products']['items'][0]['related_products']; + self::assertCount(0, $relatedProducts); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store.php new file mode 100644 index 0000000000000..b8e6d4702b44f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +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\Eav\Model\Config; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\Data\ProductLinkInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultAttributeSet = $objectManager->get(Config::class)->getEntityType(Product::ENTITY)->getDefaultAttributeSetId(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple = $productRepository->save($product); +$simple->setStoreId($storeManager->getDefaultStoreView()->getId()) +->setStatus(Status::STATUS_DISABLED); +$productRepository->save($simple); +/** @var ProductLinkInterface $productLink */ +$productLink = $objectManager->create(ProductLinkInterface::class); +$productLink->setSku('simple_with_related'); +$productLink->setLinkedProductSku('simple'); +$productLink->setPosition(1); +$productLink->setLinkType('related'); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($defaultAttributeSet) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setName('Simple Product With Related Product') + ->setSku('simple_with_related') + ->setPrice(10) + ->setWeight(18) + ->setProductLinks([$productLink]) + ->setStockData(['use_config_manage_stock' => 0]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store_rollback.php new file mode 100644 index 0000000000000..6ca27b71b9057 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_in_store_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $firstProduct = $productRepository->get('simple', false, null, true); + $productRepository->delete($firstProduct); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +try { + $secondProduct = $productRepository->get('simple_with_related', false, null, true); + $productRepository->delete($secondProduct); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 53e32483ee3d6..ceb07e3445c0e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1721,15 +1721,23 @@ public function testValidateUrlKeysMultipleStores() } /** + * Test import product with product links and empty value + * + * @param string $pathToFile + * @param bool $expectedResultCrossell + * @param bool $expectedResultUpsell + * * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_product_links_data.php * @magentoAppArea adminhtml * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @dataProvider getEmptyLinkedData */ - public function testProductLinksWithEmptyValue() - { - // import data from CSV file - $pathToFile = __DIR__ . '/_files/products_to_import_with_product_links_with_empty_value.csv'; + public function testProductLinksWithEmptyValue( + string $pathToFile, + bool $expectedResultCrossell, + bool $expectedResultUpsell + ): void { $filesystem = BootstrapHelper::getObjectManager()->create(Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -1759,8 +1767,29 @@ public function testProductLinksWithEmptyValue() $product = BootstrapHelper::getObjectManager()->create(Product::class); $product->load($productId); - $this->assertEmpty($product->getCrossSellProducts()); - $this->assertEmpty($product->getUpSellProducts()); + $this->assertEquals(empty($product->getCrossSellProducts()), $expectedResultCrossell); + $this->assertEquals(empty($product->getUpSellProducts()), $expectedResultUpsell); + } + + /** + * Get data for empty linked product + * + * @return array[] + */ + public function getEmptyLinkedData(): array + { + return [ + [ + __DIR__ . '/_files/products_to_import_with_product_links_with_empty_value.csv', + true, + true, + ], + [ + __DIR__ . '/_files/products_to_import_with_product_links_with_empty_data.csv', + false, + true, + ], + ]; } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_data.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_data.csv new file mode 100644 index 0000000000000..d8812defa0828 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_product_links_with_empty_data.csv @@ -0,0 +1,2 @@ +sku,crosssell_skus,crosssell_position,upsell_skus,upsell_position +simple,,,__EMPTY__VALUE__,__EMPTY__VALUE__ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_product_links_data.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_product_links_data.php index 5dde578a1341f..8ee35d747ea1a 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_product_links_data.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_with_product_links_data.php @@ -18,7 +18,26 @@ $objectManager = Bootstrap::getObjectManager(); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); -$product = $productRepository->get('simple_ms_1'); +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productCrosssellLink = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class); +$productCrosssellLink->setSku('simple'); +$productCrosssellLink->setLinkedProductSku('simple_ms_1'); +$productCrosssellLink->setPosition(2); +$productCrosssellLink->setLinkType('crosssell'); +$productUpsellLink = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class); +$productUpsellLink->setSku('simple'); +$productUpsellLink->setLinkedProductSku('simple_ms_1'); +$productUpsellLink->setPosition(1); +$productUpsellLink->setLinkType('upsell'); +$productRelatedLink = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class); +$productRelatedLink->setSku('simple'); +$productRelatedLink->setLinkedProductSku('simple_ms_1'); +$productRelatedLink->setPosition(3); +$productRelatedLink->setLinkType('related'); + $productModel = $objectManager->create( \Magento\Catalog\Model\Product::class ); @@ -51,10 +70,6 @@ true )->setCategoryIds( [333] -)->setUpSellLinkData( - [$product->getId() => ['position' => 1]] -)->setCrossSellLinkData( - [$product->getId() => ['position' => 2]] -)->setRelatedLinkData( - [$product->getId() => ['position' => 3]] +)->setProductLinks( + [$productCrosssellLink, $productUpsellLink, $productRelatedLink] )->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php index d2d71ab880026..abf845aca0d88 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php @@ -7,10 +7,26 @@ namespace Magento\Sales\Model\ResourceModel\Order\Grid; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; -class CollectionTest extends \PHPUnit\Framework\TestCase +class CollectionTest extends TestCase { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + /** * Tests collection properties. * @@ -19,10 +35,8 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ public function testCollectionCreate(): void { - $objectManager = Bootstrap::getObjectManager(); - /** @var Collection $gridCollection */ - $gridCollection = $objectManager->get(Collection::class); + $gridCollection = $this->objectManager->get(Collection::class); $tableDescription = $gridCollection->getConnection() ->describeTable($gridCollection->getMainTable()); @@ -42,4 +56,25 @@ public function testCollectionCreate(): void self::assertStringContainsString('main_table.', $mappedName); } } + + /** + * Verifies that filter condition date is being converted to config timezone before select sql query + * + * @return void + */ + public function testAddFieldToFilter(): void + { + $filterDate = "2021-01-19 00:00:00"; + /** @var TimezoneInterface $timeZone */ + $timeZone = $this->objectManager->get(TimezoneInterface::class); + /** @var Collection $gridCollection */ + $gridCollection = $this->objectManager->get(Collection::class); + $convertedDate = $timeZone->convertConfigTimeToUtc($filterDate); + + $collection = $gridCollection->addFieldToFilter('created_at', ['qteq' => $filterDate]); + $expectedSelect = "SELECT `main_table`.* FROM `sales_order_grid` AS `main_table` " . + "WHERE (((`main_table`.`created_at` = '{$convertedDate}')))"; + + $this->assertEquals($expectedSelect, $collection->getSelectSql(true)); + } }